Как обрабатывать круговые зависимости с RequireJS/AMD?

В моей системе у меня есть ряд "классов", загруженных в браузере, каждый отдельный файл во время разработки и объединенный вместе для производства. Когда они загружаются, они инициализируют свойство для глобального объекта, здесь G, как в этом примере:

var G = {};

G.Employee = function(name) {
    this.name = name;
    this.company = new G.Company(name + " 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");

Вместо использования моего собственного глобального объекта я планирую сделать каждый класс своим модулем AMD на основе Предложение Джеймса Берка:

define("Employee", ["Company"], function(Company) {
    return function (name) {
        this.name = name;
        this.company = new Company(name + " 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 для чего-то, для чего он не предназначен?

Ответ 1

Это действительно ограничение в формате AMD. Вы можете использовать экспорт, и эта проблема уходит. Я считаю, что экспорт является уродливым, но как обычные модули CommonJS решают проблему:

define("Employee", ["exports", "Company"], function(exports, Company) {
    function Employee(name) {
        this.name = name;
        this.company = new Company.Company(name + " 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;
});

В противном случае потребуется и запрос ( "Сотрудник" ), который вы указали в своем сообщении.

В целом с модулями вам нужно больше знать круговые зависимости, AMD или нет. Даже в простом JavaScript вы должны обязательно использовать объект, подобный объекту G в вашем примере.

Ответ 2

Я думаю, что это довольно недостаток в более крупных проектах, где (многоуровневые) круговые зависимости остаются необнаруженными. Однако с madge вы можете распечатать список круговых зависимостей для их приближения.

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

Ответ 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 + " 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");
    });

Ответ 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 должна требовать зависимости как раз вовремя (в этом случае экспортированные функции), тогда функции определения будут работать нормально. Но даже более чистая ИМО - это просто избежать круговых зависимостей вообще, что очень удобно в вашем случае.

Ответ 5

Все опубликованные ответы (кроме fooobar.com/questions/74248/...) неверны. Даже официальная документация (по состоянию на ноябрь 2014 года) неверна.

Единственное решение, которое сработало для меня, - объявить файл "гейткипер" и определить какой-либо метод, зависящий от круговых зависимостей. См. fooobar.com/questions/74253/... для конкретного примера.


Вот почему приведенные выше решения не будут работать.

  • Вы не можете:
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);
        }

    });

теперь мы можем использовать эти модули A и B в модуле C

//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

    });

Ответ 6

Я просмотрел документы по круговым зависимостям: http://requirejs.org/docs/api.html#circular

Если существует циклическая зависимость от a и b, в вашем модуле говорится о необходимости добавить в качестве зависимостей в вашем модуле так:

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

тогда, когда вам нужно "a", просто вызовите "a" так:

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

Это сработало для меня