¿Cómo manejar las dependencias circulares con RequireJS / AMD?

80

En mi sistema, tengo una serie de "clases" cargadas en el navegador, cada una de ellas en archivos separados durante el desarrollo, y concatenadas para la producción. A medida que se cargan, inicializan una propiedad en un objeto global, aquí G, como en este ejemplo:

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

En lugar de usar mi propio objeto global, estoy considerando hacer que cada clase sea su propio módulo AMD , según la sugerencia de James Burke :

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

El problema es que antes, no había dependencia de tiempo de declaración entre el Empleado y la Compañía: podía colocar la declaración en el orden que quisiera, pero ahora, usando RequireJS, esto introduce una dependencia, que está aquí (intencionalmente) circular, por lo que el falla el código anterior. Por supuesto, addEmployee()agregar una primera línea var Employee = require("Employee");lo haría funcionar , pero veo esta solución como inferior a no usar RequireJS / AMD, ya que requiere que yo, el desarrollador, esté al tanto de esta dependencia circular recién creada y haga algo al respecto.

¿Existe una mejor manera de resolver este problema con RequireJS / AMD, o estoy usando RequireJS / AMD para algo para lo que no fue diseñado?

avernet
fuente

Respuestas:

59

De hecho, esta es una restricción en el formato AMD. Podría utilizar las exportaciones y ese problema desaparece. Encuentro que las exportaciones son feas, pero así es como los módulos comunes de CommonJS resuelven el problema:

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

Otherwise, the require("Employee") you mention in your message would work too.

In general with modules you need to be more aware of circular dependencies, AMD or not. Even in plain JavaScript, you have to be sure to use an object like the G object in your example.

jrburke
fuente
3
I thought you had to declare exports in both callbacks' argument list, like function(exports, Company) and function(exports, Employee). Anyway, thanks for RequireJS, it's awsome.
Sébastien RoccaSerra
@jrburke I think this can be done one-directionally right, for a mediator or core or other top-down component? Is this a terrible idea, to make it accessible using both methods? stackoverflow.com/questions/11264827/…
SimplGy
1
I'm not sure I understand how this solves the problem. My understanding is that all the dependencies must be loaded before the define runs. Is that not the case if "exports" is passed as the first dependency?
B T
1
don't you miss exports as param in function?
shabunc
1
To follow up on @shabunc's point about the missing export param, see this question: stackoverflow.com/questions/28193382/…
Michael.Lumley
15

I think this is quite a drawback in larger projects where (multi-level) circular dependencies dwell undetected. However, with madge you can print a list of circular dependencies to approach them.

madge --circular --format amd /path/src
Pascalius
fuente
CACSVML-13295:sc-admin-ui-express amills001c$ madge --circular --format amd ./ No circular dependencies found!
Alexander Mills
8

If you don't need your dependencies to be loaded at the start (e.g., when you are extending a class), then this is what you can do: (taken from http://requirejs.org/docs/api.html#circular)

In the file a.js:

    define( [ 'B' ], function( B ){

        // Just an example
        return B.extend({
            // ...
        })

    });

And in the other file 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();
            }
        };

    });

In the OP's example, this is how it would change:

    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");
    });
redolent
fuente
2
As Gili said in his comment, this solution is wrong and will not always work. There is a race condition on which code block will be executed first.
Louis Ameline
6

I looked at the docs on circular dependencies :http://requirejs.org/docs/api.html#circular

If there is a circular dependency with a and b , it says in your module to add require as a dependency in your module like so :

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

then when you need "a" just call "a" like so:

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

This worked for me

yeahdixon
fuente
5

I would just avoid the circular dependency. Maybe something like:

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

I don't think it's a good idea to work around this issue and try to keep the circular dependency. Just feels like general bad practice. In this case it can work because you really require those modules for when the exported function is called. But imagine the case where modules are required and used in the actual definition functions itself. No workaround will make that work. That's probably why require.js fails fast on circular dependency detection in the dependencies of the definition function.

If you really have to add a work around, the cleaner one IMO is to require a dependency just in time (in your exported functions in this case), then the definition functions will run fine. But even cleaner IMO is just to avoid circular dependencies altogether, which feels really easy to do in your case.

Shu
fuente
2
You are suggesting to simplify a domain model and make it less usable just because the requirejs tool does not support that. Tools are supposed to make developer's life easier. The domain model is pretty simple - employee and company. The employee object should know what company(s) he works for, companies should have a list of employees. The domain model is right, it's the tool that fails here
Dethariel
5

All the posted answers (except https://stackoverflow.com/a/25170248/14731) are wrong. Even the official documentation (as of November 2014) is wrong.

The only solution that worked for me is to declare a "gatekeeper" file, and have it define any method that depends on the circular dependencies. See https://stackoverflow.com/a/26809254/14731 for a concrete example.


Here is why the above solutions will not work.

  1. You cannot:
var a;
require(['A'], function( A ){
     a = new A();
});

and then use a later on, because there is no guarantee that this code block will get executed before the code block that uses a. (This solution is misleading because it works 90% of the time)

  1. I see no reason to believe that exports is not vulnerable to the same race condition.

the solution to this is:

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

    });

now we can use these modules A and B in module 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

    });
Gili
fuente
btw, if you are still having trouble with this, @yeahdixon's answer should be correct, and I think the documentation itself is correct.
Alexander Mills
I agree that your methodology works but I think the documentation is correct, and might be one step closer to "synchronous".
Alexander Mills
you can because all the variables are set at load. Unless your users are time travelers and click the button before it exists. It will break causality and then a race condition is possible.
Eddie
0

In my case I solved the circular dependency by moving the code of the "simpler" object into the more complex one. For me that was a collection and a model class. I guess in your case I would add the Employee-specific parts of Company into the Employee class.

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

A bit hacky, but it should work for simple cases. And if you refactor addEmployee to take an Employee as parameter, the dependency should be even more obvious to outsiders.

Björn Tantau
fuente