【问题标题】:How to deal with cyclic dependencies in Node.js如何处理 Node.js 中的循环依赖
【发布时间】:2012-06-07 19:35:15
【问题描述】:

我最近一直在使用 nodejs,但仍然在掌握模块系统,如果这是一个明显的问题,我深表歉意。我想要的代码大致如下:

a.js(用node运行的主文件)

var ClassB = require("./b");

var ClassA = function() {
    this.thing = new ClassB();
    this.property = 5;
}

var a = new ClassA();

module.exports = a;

b.js

var a = require("./a");

var ClassB = function() {
}

ClassB.prototype.doSomethingLater() {
    util.log(a.property);
}

module.exports = ClassB;

我的问题似乎是我无法从 ClassB 的实例中访问 ClassA 的实例。

是否有正确/更好的方法来构建模块以实现我想要的? 有没有更好的方法在模块之间共享变量?

【问题讨论】:

标签: node.js module require circular-dependency cyclic-dependency


【解决方案1】:

尝试在module.exports 上设置属性,而不是完全替换它。例如,module.exports.instance = new ClassA() in a.jsmodule.exports.ClassB = ClassB in b.js。当你创建循环模块依赖时,需要的模块会从需要的模块中获得一个不完整的module.exports的引用,你可以在后面添加其他属性,但是当你设置整个module.exports时,你实际上创建了一个新对象需求模块无法访问。

【讨论】:

  • 这可能都是真的,但我想说还是要避免循环依赖。对未完全加载的模块进行特殊安排听起来会产生您不希望遇到的未来问题。这个答案规定了如何处理未完全加载的模块的解决方案......我认为这不是一个好主意。
  • 如何在不完全替换的情况下将类构造函数放入module.exports,以允许其他类“构造”该类的实例?
  • 我不认为你可以。已经导入您的模块的模块将无法看到该更改
【解决方案2】:

虽然 node.js 确实允许循环 require 依赖项,但您发现它可以是 pretty messy 并且您最好将代码重组为不需要它。也许创建第三个类,使用其他两个类来完成你需要的。

【讨论】:

  • +1 这是正确的答案。循环依赖是代码异味。如果 A 和 B 总是一起使用,它们实际上是一个单独的模块,所以合并它们。或者想办法打破依赖;也许它是一种复合模式。
  • 并非总是如此。例如,在数据库模型中,如果我有模型 A 和 B,在模型 A 中我可能想要引用模型 B(例如加入操作),反之亦然。因此,在使用“require”函数之前导出几个A和B属性(不依赖于其他模块的那些)可能是一个更好的答案。
  • 我也不认为循环依赖是代码异味。我正在开发一个系统,在某些情况下需要它。例如,建模团队和用户,其中用户可以属于多个团队。所以,并不是我的建模有问题。显然,我可以重构我的代码来避免两个实体之间的循环依赖,但这不是领域模型的最纯粹形式,所以我不会这样做。
  • 那我应该在需要的时候注入依赖吗,是这个意思吗?使用第三个来控制两个依赖之间的交互与循环问题?
  • 这不是乱七八糟的.. 有人可能想要停止文件以避免在单个文件中出现一堆代码。正如节点建议的那样,您应该在代码顶部添加exports = {},然后在代码末尾添加exports = yourData。通过这种做法,您将避免几乎所有来自循环依赖的错误。
【解决方案3】:

[编辑] 这不是 2015 年,大多数库(即 express)已经使用更好的模式进行了更新,因此不再需要循环依赖。我建议只是不要使用它们


我知道我在这里挖掘一个旧的答案...... 这里的问题是 module.exports 是在您需要 ClassB 之后定义的。 (JohnnyHK 的链接显示) 循环依赖在 Node 中工作得很好,它们只是同步定义的。 如果使用得当,它们实际上解决了很多常见的节点问题(比如从其他文件访问 express.js app

只需确保在您需要一个具有循环依赖关系的文件之前定义了必要的导出。

这将打破:

var ClassA = function(){};
var ClassB = require('classB'); //will require ClassA, which has no exports yet

module.exports = ClassA;

这将起作用:

var ClassA = module.exports = function(){};
var ClassB = require('classB');

我一直使用这种模式来访问其他文件中的 express.js app

var express = require('express');
var app = module.exports = express();
// load in other dependencies, which can now require this file and use app

【讨论】:

  • 感谢您分享模式,然后进一步分享您在导出时通常如何使用此模式app = express()
【解决方案4】:

有时引入第三类确实是人为的(正如 JohnnyHK 建议的那样),所以除了 Ianzz: 如果您确实想替换 module.exports,例如,如果您正在创建一个类(如上例中的 b.js 文件),这也是可能的,只需确保在启动循环 require,'module.exports = ...' 语句发生在 require 语句之前。

a.js(用node运行的主文件)

var ClassB = require("./b");

var ClassA = function() {
    this.thing = new ClassB();
    this.property = 5;
}

var a = new ClassA();

module.exports = a;

b.js

var ClassB = function() {
}

ClassB.prototype.doSomethingLater() {
    util.log(a.property);
}

module.exports = ClassB;

var a = require("./a"); // <------ this is the only necessary change

【讨论】:

  • 感谢 coen,我从来没有意识到 module.exports 对循环依赖有影响。
  • 这对 Mongoose (MongoDB) 模型特别有用;当 BlogPost 模型有一个引用 cmets 的数组并且每个 Comment 模型都引用 BlogPost 时,可以帮助我解决问题。
  • 这对我来说是相关模式的 mongoose 中间件实现的正确答案。按照公认答案的建议创建第三个类并不能真正解决问题,因为它仍然隐式导入类。
  • 惊人的提示!
  • 谢谢!我很感激:)
【解决方案5】:

解决方案是在需要任何其他控制器之前“转发声明”您的导出对象。因此,如果您像这样构建所有模块,您就不会遇到类似的问题:

// Module exports forward declaration:
module.exports = {

};

// Controllers:
var other_module = require('./other_module');

// Functions:
var foo = function () {

};

// Module exports injects:
module.exports.foo = foo;

【讨论】:

  • 实际上,这导致我简单地使用 exports.foo = function() {...} 代替。绝对成功了。谢谢!
  • 我不确定你在这里提出什么建议。 module.exports 默认情况下已经是一个普通的对象,所以你的“前向声明”行是多余的。
【解决方案6】:

您可以轻松解决这个问题:只需在您使用 module.exports 的模块中需要任何其他内容之前导出您的数据:

classA.js

class ClassA {

    constructor(){
        ClassB.someMethod();
        ClassB.anotherMethod();
    };

    static someMethod () {
        console.log( 'Class A Doing someMethod' );
    };

    static anotherMethod () {
        console.log( 'Class A Doing anotherMethod' );
    };

};

module.exports = ClassA;
var ClassB = require( "./classB.js" );

let classX = new ClassA();

classB.js

class ClassB {

    constructor(){
        ClassA.someMethod();
        ClassA.anotherMethod();
    };

    static someMethod () {
        console.log( 'Class B Doing someMethod' );
    };

    static anotherMethod () {
        console.log( 'Class A Doing anotherMethod' );
    };

};

module.exports = ClassB;
var ClassA = require( "./classA.js" );

let classX = new ClassB();

【讨论】:

    【解决方案7】:

    需要最小更改的解决方案是扩展 module.exports 而不是覆盖它。

    a.js - 使用 b.js* 方法的应用入口点和模块*

    _ = require('underscore'); //underscore provides extend() for shallow extend
    b = require('./b'); //module `a` uses module `b`
    _.extend(module.exports, {
        do: function () {
            console.log('doing a');
        }
    });
    b.do();//call `b.do()` which in turn will circularly call `a.do()`
    

    b.js - 使用 a.js 方法的模块

    _ = require('underscore');
    a = require('./a');
    
    _.extend(module.exports, {
        do: function(){
            console.log('doing b');
            a.do();//Call `b.do()` from `a.do()` when `a` just initalized 
        }
    })
    

    它将工作并产生:

    doing b
    doing a
    

    虽然此代码不起作用:

    a.js

    b = require('./b');
    module.exports = {
        do: function () {
            console.log('doing a');
        }
    };
    b.do();
    

    b.js

    a = require('./a');
    module.exports = {
        do: function () {
            console.log('doing b');
        }
    };
    a.do();
    

    输出:

    node a.js
    b.js:7
    a.do();
        ^    
    TypeError: a.do is not a function
    

    【讨论】:

    • 如果你没有underscore,那么ES6的Object.assign()可以做_.extend()在这个答案中所做的同样的工作。
    【解决方案8】:

    只有在需要时才需要懒惰呢?所以你的 b.js 如下所示

    var ClassB = function() {
    }
    ClassB.prototype.doSomethingLater() {
        var a = require("./a");    //a.js has finished by now
        util.log(a.property);
    }
    module.exports = ClassB;
    

    当然,将所有 require 语句放在文件顶部是一种很好的做法。但是有个场合,我原谅自己从一个原本不相关的模块中挑选了一些东西。称其为 hack,但有时这比引入进一步的依赖关系、添加额外的模块或添加新结构(EventEmitter 等)要好

    【讨论】:

    • 有时在处理带有维护对父对象引用的子对象的树数据结构时,这一点很关键。感谢您的提示。
    【解决方案9】:

    我见过的另一种方法是在第一行导出并将其保存为局部变量,如下所示:

    let self = module.exports = {};
    
    const a = require('./a');
    
    // Exporting the necessary functions
    self.func = function() { ... }
    

    我倾向于使用这种方法,你知道它有什么缺点吗?

    【讨论】:

    • 你宁愿做module.exports.func1 = , module.exports.func2 =
    【解决方案10】:

    重要的是不要重新分配给你的module.exports对象,因为那个对象可能已经给了循环中的其他模块!只需在module.exports 中分配属性,其他模块就会看到它们出现。

    所以一个简单的解决方案是:

    module.exports.firstMember = ___;
    module.exports.secondMember = ___;
    

    唯一真正的缺点是需要多次重复module.exports.


    类似于 lanzz 和 setec 的回答,我一直在使用以下模式,感觉更具声明性:

    module.exports = Object.assign(module.exports, {
        firstMember: ___,
        secondMember: ___,
    });
    

    Object.assign() 将成员复制到已分配给其他模块的 exports 对象中。

    = 赋值在逻辑上是多余的,因为它只是将module.exports 设置为自身,但我使用它是因为它帮助我的IDE(WebStorm)识别firstMember 是此模块的属性,所以“Go To -> Declaration” (Cmd-B) 和其他工具可以在其他文件中使用。

    这个模式不是很漂亮,所以我只在需要解决循环依赖问题时使用它。

    它非常适合reveal pattern,因为您可以轻松地在对象中添加和删除导出,尤其是在使用 ES6 的property shorthand 时。

    Object.assign(module.exports, {
        firstMember,
        //secondMember,
    });
    

    【讨论】:

      【解决方案11】:

      TL;DR

      只需使用exports.someMember = someMember 而不是module.exports = { // new object }

      扩展答案

      在阅读了 lanzz 的回复后,我终于可以弄清楚这里发生了什么,所以我会在这个主题上给我两分钱,扩展他的答案。

      让我们看看这个例子:

      a.js

      console.log("a starting");
      
      console.log("a requires b");
      const b = require("./b");
      console.log("a gets b =", b);
      
      function functionA() {
        console.log("function a");
      }
      
      console.log("a done");
      exports.functionA = functionA;
      

      b.js

      console.log("b starting");
      
      console.log("b requires a");
      const a = require("./a");
      console.log("b gets a =", a);
      
      function functionB() {
        console.log("On b, a =", a)
      }
      
      console.log("b done");
      exports.functionB = functionB;
      
      

      ma​​in.js

      const a = require("./a");
      const b = require("./b");
      
      b.functionB()
      

      输出

      a starting
      a requires b
      b starting
      b requires a
      b gets a = {}
      b done
      a gets b = { functionB: [Function: functionB] }
      a done
      On b, a = { functionA: [Function: functionA] }
      

      在这里我们可以看到,首先b 接收一个空对象作为a,然后一旦a 完全加载,该引用通过exports.functionA = functionA 更新。如果您通过module.exports 将整个模块替换为另一个对象,那么b 将丢失来自a 的引用,因为它将从一开始就指向同一个空对象,而不是指向新对象.

      所以如果你像这样导出amodule.exports = { functionA: functionA },那么输出将是:

      a starting
      a requires b
      b starting
      b requires a
      b gets a = {}
      b done
      a gets b = { functionB: [Function: functionB] }
      a done
      On b, a = {} // same empty object
      

      【讨论】:

        【解决方案12】:

        实际上我最终需要我的依赖

         var a = null;
         process.nextTick(()=>a=require("./a")); //Circular reference!
        

        不漂亮,但它有效。它比改变 b.js(例如只增加 modules.export)更容易理解和诚实,否则它是完美的。

        【讨论】:

        • 在此页面上的所有解决方案中,这是唯一一个解决了我的问题。我依次尝试了每个。
        【解决方案13】:

        这是我发现使用已满的快速解决方法。

        关于文件“a.js”

        let B;
        class A{
          constructor(){
            process.nextTick(()=>{
              B = require('./b')
            })
          } 
        }
        module.exports = new A();
        

        在文件'b.js'上写下以下内容

        let A;
        class B{
          constructor(){
            process.nextTick(()=>{
              A = require('./a')
            })
          } 
        }
        module.exports = new B();
        

        这样,在事件循环的下一次迭代中,将正确定义事件循环类,并且这些 require 语句将按预期工作。

        【讨论】:

        • 这不是解决方案。这只是一个计划。但这很有趣
        • @mohammadjawadBarati - “这不是解决方案”。这是一个问题的答案,那怎么不是解决方案?
        • @AlexJBallz 因为您只需要在 nextTick 中使用 b 并且这不是正确的方法。他/她必须以另一种方式改变他/她的编码风格。如果您的代码需要的东西早于它应该需要的东西,那就错了。你应该管理它不要面对这个问题或类似的事情
        • @mohammadjawadBarati 听起来你的思想有点封闭,你的方式或高速公路的口头禅在这里进行。它是一个解决方案,它有效,如果它有效并提供预期的结果,它没有错,这就是解决方案。同一个问题可以有多种解决方案,你不必喜欢它。每个人都有自己的风格。
        • 这消除了在类上拥有静态方法的能力。
        【解决方案14】:

        避免这种情况的一种方法是不需要另一个文件中的一个文件,只需将它作为参数传递给函数,无论您在另一个文件中需要什么。 这样就不会出现循环依赖了。

        【讨论】:

          【解决方案15】:

          极其简单的解决方案通常是:

          通常你会在文件的顶部有 require ...

          var script = require('./script')
          function stuff() {
                script.farfunction()
          }
          

          相反,只需要“在函数中”

          function stuff() {
                var _script = require('./script')
                _script.farfunction()
          }
          

          【讨论】:

            【解决方案16】:

            如果你只是不能消除循环依赖(例如 useraccount userlogin),还有一个选择......

            就像使用setTimeout()一样简单

            //useraccount.js
            
            let UserLogin = {};
            
            setTimeout(()=>UserLogin=require('./userlogin.js'), 10);
            
            class UserAccount{
             
            getLogin(){
            return new UserLogin(this.email);
            
            }
            
            }
            
            
            
            //userlogin.js
            
            let UserAccount ={};
            
            setTimeout(()=>UserAccount=require('./useraccount.js'), 15);
            
            
            class UserLogin{
            
            getUser(){
            
            return new User(this.token);
            
            }
            
            }
            

            【讨论】:

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