如何处理Node.js中的循环依赖关系


162

我最近一直在使用nodejs,并且仍然要处理模块系统,因此很抱歉这是一个明显的问题。我想要大致如下的代码:

a.js(主文件与节点一起运行)

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实例。

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


我建议您研究命令查询分离,可观察模式,然后研究CS专家所说的管理器-基本上是可观察模式的包装。
dewwwald

Answers:


86

尽管node.js确实允许循环require依赖,但正如您所发现的那样,它可能很杂乱,您最好重组代码以使其不需要。也许创建一个使用其他两个类来完成您需要的第三类。


6
+1这是正确的答案。循环依赖性是代码的味道。如果A和B始终一起使用,则它们实际上是一个模块,因此请合并它们。或者找到一种打破依赖的方法;也许是一种复合模式。
詹姆斯

94
不总是。在数据库模型中,例如,如果我有模型A和B,则在模型AI中可能要引用模型B(例如,加入操作),反之亦然。因此,在使用“ require”功能之前,导出几个A和B属性(不依赖于其他模块的属性)可能是一个更好的答案。
若昂·布鲁诺安博哈特姆·德利兹

11
我也没有将循环依赖项视为代码的味道。我正在开发一个在某些情况下需要它的系统。例如,为团队和用户建模,其中用户可以属于多个团队。因此,并不是我的建模有问题。显然,我可以重构代码以避免两个实体之间的循环依赖,但这不是域模型的最纯粹形式,因此我不会这样做。
亚历山大·马提尼

1
然后在需要时我应该注入依赖项,这是您的意思吗?使用三分之一来控制带有循环问题的两个依赖项之间的交互?
giovannipds

2
这并不麻烦。某人可能想要刹车一个文件,以避免编写单个文件的代码。如节点所示,您应该exports = {}在代码顶部添加一个,然后exports = yourData在代码末尾添加一个。通过这种做法,您将避免循环依赖中几乎所有的错误。
prieston

178

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


6
这可能是正确的,但我要说仍然避免循环依赖。进行特殊安排以处理声音加载不完全的模块,这将产生您将来不希望遇到的问题。这个答案为如何处理未完全加载的模块规定了解决方案...我认为这不是一个好主意。
亚历山大·米尔斯

1
您如何在module.exports不完全替换类构造函数的情况下放入它,以允许其他类“构造”该类的实例?
TimVisée'16

1
我认为你不能。已经导入了模块的模块将无法看到该更改
lanzz

52

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


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

只需确保在定义需要循环导出的文件之前定义了必要的导出即可。

这会中断:

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

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

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

2
感谢您分享此模式,然后进一步分享了导出时通常使用此模式的方式app = express()
user566245

34

有时引入第三类是真的是人为的(正如JohnnyHK所建议的那样),因此除了Ianzz:如果您确实想替换module.exports,例如,如果您正在创建一个类(例如,其中的b.js文件)。上面的示例),这也是可能的,只需确保在开始循环require的文件中,“ module.exports = ...”语句出现在require语句之前。

a.js(主文件与节点一起运行)

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

谢谢科恩,我从未意识到module.exports对循环依赖有影响。
Laurent Perrin 2013年

这在Mongoose(MongoDB)模型中特别有用;当BlogPost模型具有引用注释的数组且每个Comment模型都引用BlogPost时,可以帮助我解决问题。
Oleg Zarevennyi

14

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

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

};

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

// Functions:
var foo = function () {

};

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

3
实际上,这导致我只是简单地使用了exports.foo = function() {...}。绝对做到了。谢谢!
zanona

我不确定您在这里提出什么建议。module.exports默认情况下已经是一个普通对象,因此您的“转发声明”行是多余的。
ZachB

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

4
如果您没有underscore,则ES6 Object.assign()可以完成与_.extend()该答案相同的工作。
joeytwiddle

5

那么只有在需要时才需要懒惰吗?因此,您的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等)要好。


在处理带有子对象维护对父对象的引用的树数据结构时,有时这很关键。谢谢你的提示。
罗伯特·奥施勒

5

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

let self = module.exports = {};

const a = require('./a');

// Exporting the necessary functions
self.func = function() { ... }

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


你可以宁愿做module.exports.func1 = module.exports.func2 =
Ashwani阿加瓦尔

4

您可以轻松解决此问题:在使用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();

3

与lanzz和setect的答案类似,我一直在使用以下模式:

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

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

=分配在逻辑上是多余的,因为它只是module.exports对其自身进行设置,但是我正在使用它,因为它可以帮助我的IDE(WebStorm)识别这firstMember是该模块的属性,因此“转到->声明”(Cmd-B)其他工具也可以在其他文件中使用。

这种模式不是很漂亮,因此我仅在需要解决循环依赖问题时才使用它。


2

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

在文件“ 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语句将按预期工作。


1

实际上,我最终要求依赖

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

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


在此页面上的所有解决方案中,这是解决我的问题的唯一解决方案。我依次尝试了每个。
乔·拉普

0

避免这种情况的一种方法是,不需要其他文件,只需将其作为函数的参数传递给另一个文件即可。通过这种方式,永远不会出现循环依赖。

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.