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'); |
再执行打包,结果插件对循环依赖做出了检测,并根据我们的配置让打包失败了: