Как обрабатывать циклические зависимости с помощью RequireJS / AMD?

80

В моей системе у меня есть несколько «классов», загруженных в браузер, каждый из которых представляет собой отдельные файлы во время разработки и объединяется вместе для производства. По мере загрузки они инициализируют свойство глобального объекта 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");

Вместо использования моего собственного глобального объекта я рассматриваю возможность сделать каждый класс отдельным модулем 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");
});

Проблема в том, что раньше не было зависимости между сотрудником и компанией во время объявления: вы могли поместить объявление в любом порядке, но теперь, используя RequireJS, это вводит зависимость, которая здесь (намеренно) круговая, поэтому приведенный выше код не работает. Конечно, в addEmployee(), добавив первую строку var Employee = require("Employee");будет заставить его работать , но я вижу это решение как хуже не используя RequireJS / AMD , поскольку это требует от меня, разработчик, чтобы быть в курсе этой новой круговой зависимости и сделать что - то об этом.

Есть ли лучший способ решить эту проблему с помощью RequireJS / AMD, или я использую RequireJS / AMD для чего-то, для чего он не предназначен?

Avernet
источник

Ответы:

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

В противном случае, требование («Сотрудник»), которое вы упоминаете в своем сообщении, также будет работать.

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

Jrburke
источник
3
Я думал, вам нужно объявить экспорт в списке аргументов обоих обратных вызовов, например, function(exports, Company)и function(exports, Employee). В любом случае, спасибо за RequireJS, это потрясающе.
Себастьен РоккаСерра
@jrburke Я думаю, что это можно сделать однонаправленно, правильно, для посредника, ядра или другого нисходящего компонента? Это ужасная идея - сделать его доступным обоими методами? stackoverflow.com/questions/11264827/…
SimplGy
1
Не уверен, что понимаю, как это решает проблему. Я понимаю, что все зависимости должны быть загружены до запуска определения. Разве это не так, если "экспорт" передается как первая зависимость?
BT
1
вы не пропустите экспорт как параметр в функции?
shabunc
1
Чтобы ответить на вопрос @ shabunc об отсутствующем параметре экспорта, см. Этот вопрос: stackoverflow.com/questions/28193382/…
Майкл Ламли
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
Как сказал Гили в своем комментарии, это решение неверно и не всегда будет работать. Существует условие гонки, при котором блок кода будет выполнен первым.
Луи Амелин
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();
    }

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

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

Все опубликованные ответы (кроме https://stackoverflow.com/a/25170248/14731 ) неверны. Даже официальная документация (по состоянию на ноябрь 2014 г.) неверна.

Единственное решение, которое сработало для меня, - объявить файл «привратника» и указать ему любой метод, который зависит от циклических зависимостей. См. 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);
        }

    });

теперь мы можем использовать эти модули 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

    });
Гили
источник
кстати, если у вас все еще возникают проблемы с этим, ответ @ yeahdixon должен быть правильным, и я думаю, что сама документация верна.
Александр Миллс,
Я согласен с тем, что ваша методология работает, но я думаю, что документация верна и может быть на один шаг ближе к «синхронности».
Александр Миллс
вы можете, потому что все переменные устанавливаются при загрузке. Если только ваши пользователи не путешественники во времени и не нажимают кнопку до того, как она появится. Это нарушит причинно-следственную связь, и тогда возможно состояние гонки.
Эдди
0

В моем случае я решил круговую зависимость, переместив код «более простого» объекта в более сложный. Для меня это была коллекция и модельный класс. Думаю, в вашем случае я бы добавил части компании, относящиеся к сотрудникам, в класс сотрудников.

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 в качестве параметра, зависимость должна быть еще более очевидной для посторонних.

Бьорн Тантау
источник