模块循环依赖问题
在项目比较小的时候可能不怎么会遇到这个问题,但项目一旦有一定的体量后就可能会遇到了。
我之前做项目时就遇到这个问题,也是总结一篇文章。
比如这种类型的报错
commonjs存在的问题
先讲一下commonjs存在的问题。
CommonJS模块采用深度优先遍历,并且是加载时执行,即脚本代码在require时就全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,没有执行的部分不会输出。
举例子
js
// a.js
require("./b.js");
exports.a = function () {};
// b.js
const { a } = require("./a");
a();
// index.js
require("./a.js");
执行index.js
结果:报错a is not function
执行流程
1 导入a.js
js
require("a.js")
// 此时moduleCache
moduleCache = {
moduleA : {}
}
2 执行a.js为moduleA添加属性,发现第一行导入b.js,模块a还没执行完,执行b.js
js
require("./b.js");
// 此时moduleCache
moduleCache = {
moduleA : {},
moduleB : {}
}
3 执行b.js,发现导入a.js,此时moduleCache有moduleA,不会重复执行模块a的代码,会直接用moduleCache中模块a已经导出的内容。
js
const { a } = require("./a");
等价于
const {a} = moduleCache.moduleA
因为此时模块a的内容还未完全执行完,所以解构的变量a是undefined,还不是function,所以报错。
webpack打包结果分析
js
// a.js
import "./b.js";
export const A = () => {};
// b.js
import { A } from "./a.js";
A();
// index.js
import "./a";
webpack打包结果
js
(() => {
"use strict";
var __webpack_modules__ = {
"./src/a.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
A: () => A,
});
var _b_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./src/b.js");
var A = function A() {};
},
"./src/b.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
var _a_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./src/a.js");
(0, _a_js__WEBPACK_IMPORTED_MODULE_0__.A)();
},
};
var __webpack_module_cache__ = {};
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
(() => {
__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],
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {
value: "Module",
});
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
var __webpack_exports__ = {};
__webpack_require__.r(__webpack_exports__);
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
})();
每个模块的代码会被放到一个对象
js
var __webpack_modules__ = {
[moduleId] : 模块代码
}
js
var __webpack_modules__ = {
"./src/a.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__); // 标记模块为ES模块
__webpack_require__.d(__webpack_exports__, {
A: () => A, // getter
});
var _b_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./src/b.js");
var A = function A() {};
},
"./src/b.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
var _a_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./src/a.js");
(0, _a_js__WEBPACK_IMPORTED_MODULE_0__.A)();
},
};
webpack自定义require导入函数
js
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}
跟commonjs规范类似
- 查看缓存是否有模块导出结果,如果模块执行过了,返回模块导出结果
- 在执行模块代码之前,先创建模块导出对象module.exports
- 将模块导出对象传入执行模块代码
js
__webpack_require__.d // 定义模块导出属性
__webpack_require__.o // 检查模块导出对象是否具有某个属性
__webpack_require__.r // 标记模块为ES模块
模块代码执行前会先进行
- 将模块导出对象标记ES模块
- 如果模块有导出内容,会将这些内容定义到模块导出对象
代码执行流程
模块A执行
js
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
A: () => A, // 定义getter
});
var _b_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./src/b.js"); // 执行到这里 会暂停a模块代码执行,执行b模块
var A = function A() {};
moduleA 定义了一个A属性,A属性是一个存取器属性,有getter,getter就是返回真正导出的A。
执行b模块时,()=>A,这里返回的A还是undefined。
执行b模块
js
__webpack_require__.r(__webpack_exports__);
var _a_js__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./src/a.js");
(0, _a_js__WEBPACK_IMPORTED_MODULE_0__.A)();
跟Commonjs的问题一样,模块A还没有执行完,A还没有赋值,所以A这里是undefined,不能作为函数调用。
这里和commonjs还是有些区别
打包结果中模块代码执行前会去先定义导出属性,为属性设置一个getter,因此在代码模块执行前这些导出属性就已经在导出对象中有getter。
这里因为配置babel,打包结果会把const转成var,所以变量声明提升了,如果是const就会变成变量在声明前使用。
结论
项目会形成循环依赖实际开发中很难避免,有可能引入了某个模块就会导致形成依赖链路。
形成循环依赖链路并不一定会报错,但是在执行到对应模块时,之前模块因为导入其他包,模块代码还没完全执行完,内容还没完全导出,就有可能导致报错。
其实导致报错还好,因为可以提前在本地就感知到处理,但是如果你只是定义了一个变量,那么这个变量可能是在你还没有赋值的时候,就引用了,所以其实模块导出的变量并不是一定可信的。
其实在遇到函数调用报错时可以通过把一些函数表达式改成函数声明就好,因为打包结果模块的执行其实是执行一个函数,在执行前会有函数声明提升,但尽量不要用这种规范来处理,因为很可能会遇到更多坑。
其实有模块循环依赖后还报错,本身就是这条依赖链路有问题,应该找到不合理的地方解决,而不是去规避。用函数声明解决一些问题,反倒会留下一些坑,可能某些环境的值原本因为循环依赖导致引用时是undefined,但是碰巧你用函数声明避免了一些报错,导致埋了一个坑。
有一些工具可以分析项目中的循环链路,eslint也有相应的配置。
至于如何找到循环依赖的不合理地方就凭经验吧,这里就不展开了,毕竟是个人观点。