【问题标题】:How to handle circular dependencies with RequireJS/AMD?如何使用 RequireJS/AMD 处理循环依赖?
【发布时间】:2011-06-20 08:16:49
【问题描述】:

在我的系统中,我在浏览器中加载了许多“类”,每个“类”在开发过程中都是单独的文件,并在生产过程中连接在一起。在加载它们时,它们会初始化全局对象的属性,此处为 G,如下例所示:

var G = {};

G.Employee = function(name) {
    this.name = name;
    this.company = new G.Company(name + "'s own company");
};

G.Company = function(name) {
    this.name = name;
    this.employees = [];
};
G.Company.prototype.addEmployee = function(name) {
    var employee = new G.Employee(name);
    this.employees.push(employee);
    employee.company = this;
};

var john = new G.Employee("John");
var bigCorp = new G.Company("Big Corp");
bigCorp.addEmployee("Mary");

我正在考虑根据James Burke's suggestion 为每个类创建自己的AMD module,而不是使用我自己的全局对象:

define("Employee", ["Company"], function(Company) {
    return function (name) {
        this.name = name;
        this.company = new Company(name + "'s own company");
    };
});
define("Company", ["Employee"], function(Employee) {
    function Company(name) {
        this.name = name;
        this.employees = [];
    };
    Company.prototype.addEmployee = function(name) {
        var employee = new Employee(name);
        this.employees.push(employee);
        employee.company = this;
    };
    return Company;
});
define("main", ["Employee", "Company"], function (Employee, Company) {
    var john = new Employee("John");
    var bigCorp = new Company("Big Corp");
    bigCorp.addEmployee("Mary");
});

问题是,之前 Employee 和 Company 之间没有声明时间依赖关系:你可以按照你想要的任何顺序放置声明,但是现在,使用 RequireJS,这引入了一个依赖关系,这里(故意)循环,所以上面的代码失败了。当然,在addEmployee() 中,添加第一行var Employee = require("Employee"); 将是make it work,但我认为这个解决方案不如不使用RequireJS/AMD,因为它要求我,开发人员,意识到这个新创建的循环依赖并为此做点什么。

有没有更好的方法来使用 RequireJS/AMD 解决这个问题,或者我是否将 RequireJS/AMD 用于它不是为它设计的东西?

【问题讨论】:

    标签: javascript commonjs requirejs


    【解决方案1】:

    这确实是 AMD 格式的一个限制。你可以使用导出,这个问题就消失了。我发现导出很难看,但这是常规 CommonJS 模块解决问题的方式:

    define("Employee", ["exports", "Company"], function(exports, Company) {
        function Employee(name) {
            this.name = name;
            this.company = new Company.Company(name + "'s own company");
        };
        exports.Employee = Employee;
    });
    define("Company", ["exports", "Employee"], function(exports, Employee) {
        function Company(name) {
            this.name = name;
            this.employees = [];
        };
        Company.prototype.addEmployee = function(name) {
            var employee = new Employee.Employee(name);
            this.employees.push(employee);
            employee.company = this;
        };
        exports.Company = Company;
    });
    

    否则,您在消息中提到的 require("Employee") 也会起作用。

    通常,对于模块,您需要更加了解循环依赖关系,无论 AMD 与否。即使在纯 JavaScript 中,您也必须确保在示例中使用像 G 对象这样的对象。

    【讨论】:

    • 我认为您必须在两个回调的参数列表中声明导出,例如 function(exports, Company)function(exports, Employee)。无论如何,感谢 RequireJS,这太棒了。
    • @jrburke 我认为这可以单向正确地完成,对于中介或核心或其他自上而下的组件?这是一个糟糕的想法,使用这两种方法都可以访问吗? stackoverflow.com/questions/11264827/…
    • 我不确定我是否理解这如何解决问题。我的理解是必须在定义运行之前加载所有依赖项。如果“exports”作为第一个依赖项传递,情况不是这样吗?
    • 你不会错过exports as param in function吗?
    • 要跟进@shabunc 关于缺少导出参数的观点,请参阅以下问题:stackoverflow.com/questions/28193382/…
    【解决方案2】:

    我认为这在(多级)循环依赖项未被检测到的大型项目中是一个相当大的缺点。 但是,使用madge,您可以打印一个循环依赖列表来接近它们。

    madge --circular --format amd /path/src
    

    【讨论】:

    • CACSVML-13295:sc-admin-ui-express amills001c$ madge --circular --format amd ./ 未找到循环依赖关系!
    【解决方案3】:

    如果您不需要在开始时加载依赖项(例如,当您扩展一个类时),那么您可以这样做:(取自 http://requirejs.org/docs/api.html#circular

    在文件a.js

        define( [ 'B' ], function( B ){
    
            // Just an example
            return B.extend({
                // ...
            })
    
        });
    

    在另一个文件b.js:

        define( [ ], function( ){ // Note that A is not listed
    
            var a;
            require(['A'], function( A ){
                a = new A();
            });
    
            return function(){
                functionThatDependsOnA: function(){
                    // Note that 'a' is not used until here
                    a.doStuff();
                }
            };
    
        });
    

    在 OP 的示例中,这就是它的变化方式:

        define("Employee", [], function() {
    
            var Company;
            require(["Company"], function( C ){
                // Delayed loading
                Company = C;
            });
    
            return function (name) {
                this.name = name;
                this.company = new Company(name + "'s own company");
            };
        });
    
        define("Company", ["Employee"], function(Employee) {
            function Company(name) {
                this.name = name;
                this.employees = [];
            };
            Company.prototype.addEmployee = function(name) {
                var employee = new Employee(name);
                this.employees.push(employee);
                employee.company = this;
            };
            return Company;
        });
    
        define("main", ["Employee", "Company"], function (Employee, Company) {
            var john = new Employee("John");
            var bigCorp = new Company("Big Corp");
            bigCorp.addEmployee("Mary");
        });
    

    【讨论】:

    • 正如 Gili 在他的评论中所说,这个解决方案是错误的,并且不会总是有效。存在将首先执行代码块的竞争条件。
    【解决方案4】:

    我只想避免循环依赖。可能是这样的:

    G.Company.prototype.addEmployee = function(employee) {
        this.employees.push(employee);
        employee.company = this;
    };
    
    var mary = new G.Employee("Mary");
    var bigCorp = new G.Company("Big Corp");
    bigCorp.addEmployee(mary);
    

    我认为解决这个问题并尝试保持循环依赖不是一个好主意。感觉就像一般的坏习惯。在这种情况下,它可以工作,因为您在调用导出函数时确实需要这些模块。但是想象一下在实际定义函数本身中需要和使用模块的情况。没有任何解决方法可以使这项工作。这可能就是 require.js 在定义函数的依赖项中循环依赖检测失败的原因。

    如果你真的需要添加一个变通方法,更简洁的 IMO 是及时要求一个依赖项(在这种情况下是在你的导出函数中),那么定义函数将运行良好。但即使是更干净的 IMO 也只是为了完全避免循环依赖,这在你的情况下感觉很容易做到。

    【讨论】:

    • 您建议简化域模型并使其不那么可用,只是因为 requirejs 工具不支持它。工具应该让开发人员的生活更轻松。领域模型非常简单——员工和公司。员工对象应该知道他为哪个公司工作,公司应该有一个员工列表。领域模型是对的,它是这里失败的工具
    【解决方案5】:

    所有发布的答案(https://stackoverflow.com/a/25170248/14731 除外)都是错误的。甚至官方文档(截至 2014 年 11 月)也是错误的。

    对我有用的唯一解决方案是声明一个“看门人”文件,并让它定义任何依赖于循环依赖的方法。具体示例见https://stackoverflow.com/a/26809254/14731


    这就是上述解决方案不起作用的原因。

    1. 你不能:
    var a;
    require(['A'], function( A ){
         a = new A();
    });
    

    然后再使用a,因为不能保证这个代码块会在使用a的代码块之前执行。 (此解决方案具有误导性,因为它 90% 的时间都有效)

    1. 我认为没有理由相信 exports 不会受到相同竞争条件的影响。

    解决办法是:

    //module A
    
        define(['B'], function(b){
    
           function A(b){ console.log(b)}
    
           return new A(b); //OK as is
    
        });
    
    
    //module B
    
        define(['A'], function(a){
    
             function B(a){}
    
             return new B(a);  //wait...we can't do this! RequireJS will throw an error if we do this.
    
        });
    
    
    //module B, new and improved
        define(function(){
    
             function B(a){}
    
           return function(a){   //return a function which won't immediately execute
                  return new B(a);
            }
    
        });
    

    现在我们可以在模块 C 中使用这些模块 A 和 B

    //module C
        define(['A','B'], function(a,b){
    
            var c = b(a);  //executes synchronously (no race conditions) in other words, a is definitely defined before being passed to b
    
        });
    

    【讨论】:

    • 顺便说一句,如果您仍然遇到问题,@yeahdixon 的回答应该是正确的,我认为文档本身是正确的。
    • 我同意您的方法有效,但我认为文档是正确的,并且可能更接近“同步”。
    • 可以,因为所有变量都是在加载时设置的。除非您的用户是时间旅行者并在按钮存在之前单击它。它将打破因果关系,然后可能出现竞争条件。
    【解决方案6】:

    我查看了有关循环依赖的文档:http://requirejs.org/docs/api.html#circular

    如果存在与 a 和 b 的循环依赖关系,它会在您的模块中说明在您的模块中添加 require 作为依赖项,如下所示:

    define(["require", "a"],function(require, a) { ....
    

    然后当你需要“a”时,只需像这样调用“a”:

    return function(title) {
            return require("a").doSomething();
        }
    

    这对我有用

    【讨论】:

      【解决方案7】:

      在我的例子中,我通过将“更简单”对象的代码移动到更复杂的对象中来解决循环依赖问题。对我来说,那是一个集合和一个模型类。我想在您的情况下,我会将 Company 的 Employee 特定部分添加到 Employee 类中。

      define("Employee", ["Company"], function(Company) {
          function Employee (name) {
              this.name = name;
              this.company = new Company(name + "'s own company");
          };
          Company.prototype.addEmployee = function(name) {
              var employee = new Employee(name);
              this.employees.push(employee);
              employee.company = this;
          };
      
          return Employee;
      });
      define("Company", [], function() {
          function Company(name) {
              this.name = name;
              this.employees = [];
          };
          return Company;
      });
      define("main", ["Employee", "Company"], function (Employee, Company) {
          var john = new Employee("John");
          var bigCorp = new Company("Big Corp");
          bigCorp.addEmployee("Mary");
      });
      

      有点hacky,但它应该适用于简单的情况。如果你重构addEmployee 以将Employee 作为参数,那么对于外人来说,这种依赖关系应该会更加明显。

      【讨论】:

        猜你喜欢
        • 2014-07-29
        • 2014-09-22
        • 1970-01-01
        • 1970-01-01
        • 2016-05-02
        • 1970-01-01
        • 2011-07-26
        • 2015-01-04
        • 2018-10-05
        相关资源
        最近更新 更多