什么是循环依赖
循环依赖一般会伴随模块化一起出现,就是在a模块中依赖b模块,而b模块又依赖a模块。
在以前开发时就遇到过这种情况,在store初始化时使用了utils模块中的方法,而utils中有利用到了store中的数据
在开发时没有问题,但是在打包后运行时就报错了,实际上就是遇到了循环依赖的问题。
对于循环依赖,Node默认的CommonJS模块和ES6的模块以及Webpack打包时的处理各不相同
CommonJS中对循环依赖的处理
可以看Node官方对循环依赖的介绍:
a.js,通过require,引用了b模块:
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模块:
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中,先后引用a和b模块:
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,输出结果会是什么呢:
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.js的exports的副本(unfinished copy)会作为require('a')的结果返回给b
所以这时候,在b中a.done是false,执行完成b后,继续执行a模块,a模块的b.done也就变成了true
从上面的例子可以看出来,CommonJS模块对循环依赖进行了很好的处理,主要依赖于它的两个特点:
- 模块在运行时加载
- 会缓存已加载(包括未完成的)模块
ESM的处理
在a.mjs中通过import获取b.mjs中导出的bar,b.mjs通过import获取a.mjs中的foo
.mjs用来标识使用了ES6 Modoule
a.mjs中:
import {bar} from './b.mjs';
console.log('a.mjs');
console.log(bar);
export const foo = 'foo';
b.mjs中:
import {foo} from './a.mjs';
console.log('b.mjs');
console.log(foo);
export const bar = 'bar';
然后在Node环境下执行:
node --experimental-modules a.mjs
执行结果:
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拥有了变量提升,输出结果就发生了变化,不在报错:
b.mjs
undefined
a.mjs
bar
上面的结果也是符合ESM的特性:
- ESM模块输出的是值的引用
- 输出接口动态执行
- 静态接口
Webpack对循环依赖的处理
首先安装了Webpack和Webpack-CLI:
npm install webpack webpack-cli -D
然后在项目中新建了webpack.config.js配置文件:
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命令后:
> 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这个插件:
npm i circular-dependency-plugin -D
然后在webpack.config.js中添加如下配置:
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()
})
]
};
再执行打包,结果插件对循环依赖做出了检测,并根据我们的配置让打包失败了:
