JavaScript中循环依赖的处理

JavaScript中循环依赖的处理。

什么是循环依赖

循环依赖一般会伴随模块化一起出现,就是在a模块中依赖b模块,而b模块又依赖a模块。

在以前开发时就遇到过这种情况,在store初始化时使用了utils模块中的方法,而utils中有利用到了store中的数据

在开发时没有问题,但是在打包后运行时就报错了,实际上就是遇到了循环依赖的问题。

对于循环依赖,Node默认的CommonJS模块和ES6的模块以及Webpack打包时的处理各不相同

CommonJS中对循环依赖的处理

可以看Node官方对循环依赖的介绍

a.js,通过require,引用了b模块:

1
2
3
4
5
6
7
8
console.log('a start');
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中,通过require引用了a模块:

1
2
3
4
5
6
7
8
console.log('b start');
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中,先后引用ab模块:

1
2
3
4
5
6
console.log('main start');

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

console.log('in main, a.done = %j, b.done = %j', a.done, b.done);

然后执行node main.js,输出结果会是什么呢:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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

在运行a模块是,遇到了require('b'),就去运行b.js,在里面又遇到了require('a'),为了避免循环引用,一个未执行完成的a.jsexports的副本(unfinished copy)会作为require('a')的结果返回给b

所以这时候,在ba.donefalse,执行完成b后,继续执行a模块,a模块的b.done也就变成了true

从上面的例子可以看出来,CommonJS模块对循环依赖进行了很好的处理,主要依赖于它的两个特点:

  1. 模块在运行时加载
  2. 会缓存已加载(包括未完成的)模块

ESM的处理

a.mjs中通过import获取b.mjs中导出的barb.mjs通过import获取a.mjs中的foo

.mjs用来标识使用了ES6 Modoule

a.mjs中:

1
2
3
4
5
6
import {bar} from './b.mjs';

console.log('a.mjs');
console.log(bar);

export const foo = 'foo';

b.mjs中:

1
2
3
4
5
6
import {foo} from './a.mjs';

console.log('b.mjs');
console.log(foo);

export const bar = 'bar';

然后在Node环境下执行:

1
node --experimental-modules a.mjs

执行结果:

1
2
3
4
b.mjs
console.log(foo);
^
ReferenceError: Cannot access 'foo' before initialization

根据阮一峰老师的讲解,在执行a.mjs后,引擎发现加载了b.mjs,然后优先执行b.mjs

在执行b.mjs时,发现从a中导入了foo,这时不会去执行a.mjs,会认为foo已经存在,继续完成执行,直到运行到console.log(foo)时,才发现foo根本没定义,所以报错了

如果将a.mjs中的最后一个变量的声明有const改为var,由于foo拥有了变量提升,输出结果就发生了变化,不在报错:

1
2
3
4
b.mjs
undefined
a.mjs
bar

上面的结果也是符合ESM的特性:

  1. ESM模块输出的是值的引用
  2. 输出接口动态执行
  3. 静态接口

Webpack对循环依赖的处理

首先安装了WebpackWebpack-CLI

1
npm install webpack webpack-cli -D

然后在项目中新建了webpack.config.js配置文件:

1
2
3
4
5
6
7
8
9
const path = require('path');

module.exports = {
entry: path.resolve(__dirname, 'demo12/commonjs/index.js'),
output: {
path: path.resolve(__dirname, 'demo12/dist'),
filename: 'my-first-webpack.bundle.js'
},
};

配置了一个最简单的打包配置,然后运行package.json中配置好的webpack命令后:

1
2
3
4
5
6
> webpack

asset my-first-webpack.bundle.js 533 bytes [compared for emit] [minimized] (name: main)
./demo12/commonjs/index.js 193 bytes [built] [code generated]
./demo12/commonjs/a.js 203 bytes [built] [code generated]
./demo12/commonjs/b.js 203 bytes [built] [code generated]

Webpack没有对循环依赖做出任何检测,打包过程也没有任何报错,在浏览器中执行打包后的结果,与CommonJS的结果完全相同

如果需要让Webpack对循环依赖做出检测,需要使用circular-dependency-plugin这个插件:

1
npm i circular-dependency-plugin -D

然后在webpack.config.js中添加如下配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const path = require('path');
const CircularDependencyPlugin = require('circular-dependency-plugin');

module.exports = {
entry: path.resolve(__dirname, 'demo12/commonjs/index.js'),
output: {
path: path.resolve(__dirname, 'demo12/dist'),
filename: 'my-first-webpack.bundle.js'
},
plugins: [
new CircularDependencyPlugin({
exclude: /node_modules/,
include: /demo12/,
failOnError: true,
allowAsyncCycles: false,
cwd: process.cwd()
})
]
};

再执行打包,结果插件对循环依赖做出了检测,并根据我们的配置让打包失败了:

参考