如何使用RequireJS / AMD处理循环依赖关系?


79

在我的系统中,我在浏览器中加载了许多“类”,每个类在开发过程中都是一个单独的文件,并串联在一起进行生产。加载它们时,它们在全局对象(这里)上初始化一个属性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的建议,而不是使用自己的全局对象,而是考虑使每个类都具有自己的AMD模块

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");使它的工作,但我看到这个解决方案,不如不使用RequireJS / AMD,因为它需要我,开发商,要知道这个新创建循环依赖,并做一些事情。

是否有更好的方法来解决RequireJS / AMD的问题,还是我将RequireJS / AMD用于并非为它设计的东西?

Answers:


59

这确实是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对象的对象。


3
我认为您必须在两个回调的参数列表中声明出口,例如function(exports, Company)function(exports, Employee)。无论如何,感谢RequireJS,这太棒了。
塞巴斯蒂安RoccaSerra

@jrburke我认为对于介体,核心或其他自上而下的组件,可以单向完成此操作吗?使这两种方法都可访问这是一个可怕的想法吗?stackoverflow.com/questions/11264827/...
SimplGy

1
我不确定我是否能解决问题。我的理解是,必须在定义运行之前加载所有依赖项。如果将“ exports”作为第一个依赖项传递,那不是这种情况吗?
英国电信

1
您不错过出口作为功能的参数吗?
shabunc

1
要跟进@shabunc关于缺失的出口参数的观点,请参见以下问题:stackoverflow.com/questions/28193382/…–
Michael.Lumley

15

我认为这在较大的项目中是一个很大的缺陷,在这些项目中,(多级)循环依赖项没有被发现。但是,使用madge可以打印循环依赖关系列表以接近它们。

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

CACSVML-13295:sc-admin-ui-express amills001c $ madge --circular --format amd ./找不到循环依赖项!
亚历山大·米尔斯

8

如果您不需要一开始就加载依赖项(例如,在扩展类时),那么您可以执行以下操作:(摘自http://requirejs.org/docs/api.html#圆形

在文件中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");
    });

2
正如吉利(Gili)在评论中说的那样,这种解决方案是错误的,并不总是可行。在竞争条件下,将首先执行代码块。
路易·阿梅琳

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();
    }

这对我有用


5

我只是避免循环依赖。也许像这样:

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也只是为了完全避免循环依赖,这在您的情况下确实很容易做到。


2
您建议简化域模型并降低其可用性,因为requirejs工具不支持该模型。工具应该可以简化开发人员的生活。域模型非常简单-员工和公司。员工对象应该知道他在哪个公司工作,公司应该有一个员工列表。域模型是正确的,这是此处失败的工具
Dethariel

5

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

对我有用的唯一解决方案是声明一个“ gatekeeper”文件,并让它定义任何依赖循环依赖的方法。有关具体示例,请参见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的答案应该是正确的,我认为文档本身是正确的。
亚历山大·米尔斯

我同意您的方法有效,但是我认为文档是正确的,可能离“同步”更近了一步。
亚历山大·米尔斯

您可以,因为所有变量都在加载时设置。除非您的用户是时间旅行者,否则请在该按钮存在之前单击它。它将打破因果关系,然后可能出现竞争状况。
艾迪(Eddie)

0

在我的情况下,我通过将“简单”对象的代码移到更复杂的对象中来解决了循环依赖性。对我来说,这是一个收藏和一个模型课。我想在您的情况下,我会将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");
});

有点古怪,但它应该适用于简单的情况。而且,如果您重构addEmployee为将Employee作为参数,则对外部人员的依赖性应该更加明显。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.