JavaScript中循环依赖的处理
JavaScript中循环依赖的处理。
什么是循环依赖
循环依赖一般会伴随模块化一起出现,就是在a
模块中依赖b
模块,而b
模块又依赖a
模块。
在以前开发时就遇到过这种情况,在store
初始化时使用了utils
模块中的方法,而utils
中有利用到了store
中的数据
在开发时没有问题,但是在打包后运行时就报错了,实际上就是遇到了循环依赖的问题。
对于循环依赖,Node默认的CommonJS模块和ES6的模块以及Webpack打包时的处理各不相同
CommonJS中对循环依赖的处理
可以看Node官方对循环依赖的介绍:
a.js
,通过require
,引用了b
模块:
1 | console.log('a start'); |
在b.js
中,通过require
引用了a
模块:
1 | console.log('b start'); |
在main.js
中,先后引用a
和b
模块:
1 | console.log('main start'); |
然后执行node main.js
,输出结果会是什么呢:
1 | main starting |
在运行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
中:
1 | import {bar} from './b.mjs'; |
b.mjs
中:
1 | import {foo} from './a.mjs'; |
然后在Node环境下执行:
1 | node --experimental-modules a.mjs |
执行结果:
1 | b.mjs |
根据阮一峰老师的讲解,在执行a.mjs
后,引擎发现加载了b.mjs
,然后优先执行b.mjs
。
在执行b.mjs
时,发现从a
中导入了foo
,这时不会去执行a.mjs
,会认为foo
已经存在,继续完成执行,直到运行到console.log(foo)
时,才发现foo
根本没定义,所以报错了
如果将a.mjs
中的最后一个变量的声明有const
改为var
,由于foo
拥有了变量提升,输出结果就发生了变化,不在报错:
1 | b.mjs |
上面的结果也是符合ESM的特性:
- ESM模块输出的是值的引用
- 输出接口动态执行
- 静态接口
Webpack对循环依赖的处理
首先安装了Webpack
和Webpack-CLI
:
1 | npm install webpack webpack-cli -D |
然后在项目中新建了webpack.config.js
配置文件:
1 | const path = require('path'); |
配置了一个最简单的打包配置,然后运行package.json
中配置好的webpack
命令后:
1 | > webpack |
Webpack没有对循环依赖做出任何检测,打包过程也没有任何报错,在浏览器中执行打包后的结果,与CommonJS的结果完全相同
如果需要让Webpack对循环依赖做出检测,需要使用circular-dependency-plugin这个插件:
1 | npm i circular-dependency-plugin -D |
然后在webpack.config.js
中添加如下配置:
1 | const path = require('path'); |
再执行打包,结果插件对循环依赖做出了检测,并根据我们的配置让打包失败了: