什么是循环依赖?
循环依赖是指模块A依赖于模块B,同时模块B依赖于模块A。比如下面这个例子:
// foo.js
const bar = require('./bar.js');
console.log('value of bar:', bar);
module.exports = 'This is foo.js';
// bar.js
const foo = require('./foo.js');
console.log('value of foo:', foo);
module.exports = 'This is bar.js';
// index.js
require('./foo.js');
在这里,index.js
是执行入口,它加载了foo.js
,foo.js
和bar.js
之间存在循环依赖
理想状态下我们希望控制台上输出:
value of foo: This is foo.js
value of bar: This is bar.js
实际输出却是:
value of foo: {}
value of bar: This is bar.js
为什么foo的值会是一个空对象呢?让我们从头梳理一下代码的实际执行顺序。
- index.js导入了foo.js,此时开始执行foo.js中的代码。
- foo.js的第1句导入了bar.js,这时foo.js不会继续向下执行,而是进入了bar.js内部。
- 在bar.js中又对foo.js进行了require,这里产生了循环依赖。需要注意的是,执行权并不会再交回foo.js,而是直接取其导出值,也就是module.exports。但由于foo.js未执行完毕,导出值在这时为默认的空对象,因此当bar.js执行到打印语句时,我们看到控制台中的value of foo就是一个空对象。
- bar.js执行完毕,将执行权交回foo.js。
- foo.js从require语句继续向下执行,在控制台打印出value of bar(这个值是正确的),整个流程结束。
由上面可以看出,尽管循环依赖的模块均被执行了,但模块导入的值并不是我们想要的。因此在CommonJS中,若遇到循环依赖我们没有办法得到预想中的结果。
我们再从Webpack的实现角度来看,将上面例子打包后,bundle中有这样一段代码非常重要:
// The require function
function __webpack_require__(moduleId) {
if(installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// Create a new module (and put it into the cache)
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
...
}
当index.js引用了foo.js之后,相当于执行了这个__webpack_require__函数,初始化了一个module对象并放入installedModules中。当bar.js再次引用foo.js时,又执行了该函数,但这次是直接从installedModules里面取值,此时它的module.exports是一个空对象。这就解释了上面在第3步看到的现象。
接下来让我们使用ES6 Module的方式重写上面的例子。
// foo.js
import bar from './bar.js';
console.log('value of bar:', bar);
export default 'This is foo.js';
// bar.js
import foo from './foo.js';
console.log('value of foo:', foo);
export default 'This is bar.js';
// index.js
import foo from './foo.js';
执行结果如下:
value of foo: undefined
foo.js:3 value of bar: This is bar.js
很遗憾,在bar.js中同样无法得到foo.js正确的导出值,只不过和CommonJS默认导出一个空对象不同,这里获取到的是undefined。
那怎么才能规避循环依赖呢!
避免循环依赖的方法
//index.js
import foo from './foo.js';
foo('index.js');
// foo.js
import bar from './bar.js';
function foo(invoker) {
console.log(invoker + ' invokes foo.js');
bar('foo.js');
}
export default foo;
// bar.js
import foo from './foo.js';
let invoked = false;
function bar(invoker) {
if(!invoked) {
invoked = true;
console.log(invoker + ' invokes bar.js');
foo('bar.js');
}
}
export default bar;
上面代码的执行结果如下:
index.js invokes foo.js
foo.js invokes bar.js
bar.js invokes foo.js
ES6 Module的特性使其可以更好地支持循环依赖,只是需要由开发者来保证当导入的值被使用时已经设置好正确的导出值。