Node.js 中的模块循环依赖

853 阅读2分钟

最近在开发Node.js项目时,遇到了循环依赖(circular dependcy)的问题,如下图所示。

image.png 具体来说,就是在settings.js文件中引用了sessions.js文件和utils文件中的某些函数,settings.js文件和utils.js文件相互引用,这导致了settings.js无法获取sessions.js中的方法,从而使得程序报错,无法运行。这在我们项目达到了一定的复杂度之后,是很难避免的,而如果开发人员不了解循环依赖,会浪费很多debug的时间。

// settings.js
const { a } = require('./sessions')
const { b } = require('./utils')

// sessions.js
const { c } = require('./utils')

// utils.js
const { d } = require('./settings')
  • CommonJS模块的加载原理

Node.js使用CommonJS模块规范,所以在介绍循环依赖之前,先来了解一下CommonJS模块的加载原理。CommonJS模块的重要特性是加载时执行,require命令第一次加载该脚本,就会执行整个脚本,然后在内存生成一个对象,如下所示:

{
    id: 'xxx',
    exports: { ... },
    loaded: true,
}

上面代码中,该对象的id属性是模块名,exports属性是模块输出的各个接口,初始化是一个空对象,loaded属性是一个布尔值,表示该模块的脚本是否执行完毕。该对象会被缓存,之后再次使用到该模块的时候会直接从缓存中取值。

  • 循环依赖问题解析

Node官方文档给出了对于循环依赖的说明,并给了一个例子:

a.js

console.log('a starting');
exports.done = false;
const b = require('./b.js');
console.log('in a, b.done = %j', b.done);
exports.done = true;
console.log('a done');

b.js

console.log('b starting');
exports.done = false;
const a = require('./a.js');
console.log('in b, a.done = %j', a.done);
exports.done = true;
console.log('b done');

main.js

console.log('main starting');
const a = require('./a.js');
const b = require('./b.js');
console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

从上面的代码中可以看到,main.js引用了a.jsb.js,并且a.jsb.js相互引用,两者是循环依赖,当执行main.js时输出如下:

$ node main.js
main starting
a starting
b starting
in b, a.done = false
b done
in a, b.done = true
a done
in main, a.done = true, b.done = true

根据结果可以看出,main.js首先会require a.js,此时执行到const b = require('./b.js')的时候,程序会转去require b.js,在b.js中执行到const a = require('./a.js'),为了防止无限循环,系统会去a.js模块对应对象的exports属性取值,可是因为a.js还没有执行完,从exports属性只能取回已经执行的部分,而不是最后的值。然后b.js完成加载,并将其导出对象提供给a.js模块

  • 解决循环依赖问题

根据上面的分析可以得出,之所以在settings.js中无法获取sessions.js中的方法,是因为三个js文件形成了相互依赖关系,导致在require sessions.js时,sessions.js模块还没有导出正确的函数,此时获取到的函数为undefined,所以程序报错。

弄清楚原因之后,解决问题就很简单了,只需要在utils.jsrequire settings.js的时机尽量往后放,不放到文件头部,把自身需要导出的部分都导出了再require settings.js。这种写法有一个问题就是会令开发者感到不快,因为它与我们日常开发的书写顺序不符,所以注释就显得格外重要了