循环依赖相信大家都不陌生了,没了解过的可以看看# JavaScript 模块的循环加载 做一下了解,这里不再赘述 。
CommonJS 和 ES6 的模块运行机制不一样, 处理方式分别为
- CommonJS的做法是,一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
- ES6模块的运行机制与CommonJS不一样,它遇到模块加载命令
import时,不会去执行模块,而是只生成一个引用。等到真的需要用到时,再到模块里面去取值。 ES6根本不会关心是否发生了"循环加载",只是生成一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
webpack 自己实现了一套模块加载机制,可能很多人都遇到因为循环依赖导致的 undefined 问题,那么我们来看看它又是如何处理循环依赖的。
我们准备了 3 个文件,代码如下
// index.js
import './a';
// a.js
import { bar } from './b';
console.log('a run ')
const a = 'aaaa'
export function foo() {
console.log('执行完毕', a);
bar();
}
export const b = a;
foo();
// b.js
import { foo } from './a';
console.log('b run');
export const bar = () => {
console.log('bar...')
}
foo();
a 和 b 互相依赖
编辑后执行会报®
我们先来简单回顾一下 webpack 的 module 实现
/**
* __webpack_module_cache__: 缓存了所有 module 的执行结果
* __webpack_modules__: 保存了所有 module
*/
var __webpack_modules__ = ({
"./src/a.js": (module, module.export, __webpack_require__) => void;
"./src/b.js": (module, module.export, __webpack_require__) => void;
"./src/index.js": (module, module.export, __webpack_require__) => void;
})
// The module cache
var __webpack_module_cache__ = {};
// The require function
function __webpack_require__(moduleId) {
// Check if module is in cache
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// Create a new module (and put it into the cache)
var module = __webpack_module_cache__[moduleId] = {
// no module.id needed
// no module.loaded needed
exports: {}
};
// Execute the module function
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// Return the exports of the module
return module.exports;
}
__webpack_require__("./src/index.js");
看看编译后的代码,就知道之前执行的时候为什么会报错了
有 2 个关键点需要注意
1,exports 会被提到最前面,通过 Object.defineProperty 将模块需要导出的变量添加到 module.exports,通过 get 方法保存了引用,在使用时才会去读取
2,js 变量提升
之前为什么执行会报错呢?
我们看到 const a = 'aaaa' 是在 var bModule = __webpack_require__("./src/b.js"); 之后定义的。
const 并不存在变提升
而 a.js 的 foo 方法作为一个函数函数声明和初始化都会被提升
这就解释了在 b.js 中调用 aModule.foo(); 时,为什么会出现 ReferenceError: Cannot access 'a' before initialization 的错误了。
你可以试试将 const a 改成 var a 看看有什么效果
"./src/a.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
/**
* __webpack_require__.d 的作用:
* 调用 Object.defineProperty 将 模块需要导出的变量添加到 module.exports,通过 get 方法保存了引用,在使用时才会去读取
*
*/
__webpack_require__.d(__webpack_exports__, {
"b": () => (b),
"foo": () => (foo)
});
var bModule = __webpack_require__("./src/b.js");
console.log('a run ')
const a = 'aaaa'
function foo() {
console.log('执行完毕', a);
bModule.bar();
}
const b = a;
foo();
}),
"./src/b.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.d(__webpack_exports__, {
"bar": () => (bar)
});
var aModule = __webpack_require__("./src/a.js");
console.log('b run');
const bar = () => {
console.log('bar...')
}
aModule.foo();
}),
__webpack_require__.d = (exports, definition) => {
for(var key in definition) {
if(__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, { enumerable: true, get: definition[key] });
}
}
};
前面的代码可能有点复杂,看起来不方便,我写个段简化版本,帮助理解
const exportTest = {
getLibrary: (key) => {
return getLibrary(key);
}
}
// 模拟别的模块调用本模块的 getLibrary 方法
console.log(exportTest.getLibrary("React"));
var library = {
React: 'React',
Vue: 'Vue'
}
function getLibrary (key) {
return library[key];
}
个人感觉虽然 webpack 编译后的代码看着像 CommonJS,但是处理循环依赖的方式跟 ES6 比较类似(不一定对,没去细查)
在平时开发中,应尽量避免循环依赖,如果出现了,多关注下JS 变量提升 很多时候都是它的锅
另外也可以使用 dependency-cruiser 来检测出项目中的循环依赖