# 实战解析
# 1. 多次引用的模块是如何处理的?会重复加载嘛?
不会,这个问题主要是涉及webpack的模块化机制。 首先看个案例,a.js
export default function(){
console.log('a');
}
1
2
3
2
3
b.js
import a from './a';
export default function(){
console.log('我是b');
a();
}
1
2
3
4
5
2
3
4
5
入口文件index.js
import a from './a';
import b from './b';
a();
b();
1
2
3
4
2
3
4
最后webpack编译的chunk如下:
(function(modules) { // webpackBootstrap
//模块方法定义
return __webpack_require__(__webpack_require__.s = "./src/index.js");
({
"./src/a.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n console.log('a');\n});\n\n//# sourceURL=webpack:///./src/a.js?");
}),
"./src/b.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./src/a.js\");\n\n/* harmony default export */ __webpack_exports__[\"default\"] = (function(){\n console.log('我是b');\n Object(_a__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\n});\n\n//# sourceURL=webpack:///./src/b.js?");
}),
"./src/index.js":
(function(module, __webpack_exports__, __webpack_require__) {
"use strict";
eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./a */ \"./src/a.js\");\n/* harmony import */ var _b__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(/*! ./b */ \"./src/b.js\");\n// function component() {\n// var element = document.createElement('div');\n// // Lodash(目前通过一个 script 脚本引入)对于执行这一行是必需的\n// element.innerHTML = _.join(['Hello', 'webpack'], ' ');\n// return element;\n// }\n \n// document.body.appendChild(component());\n\n\n\nObject(_a__WEBPACK_IMPORTED_MODULE_0__[\"default\"])();\nObject(_b__WEBPACK_IMPORTED_MODULE_1__[\"default\"])();\n\n//# sourceURL=webpack:///./src/index.js?");
})
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
webpack在解析入口文件时,会通过入口文件来逐步分析模块依赖。最后生成的模板是一个立即执行包裹的函数,重新定义了__webpack_require__方法,模块依赖会按照各自标识的方式作为参数传递给函数。 上面编译出来的代码主要包含两个部分:Runtime,模块。上半部分就是Runtime,作用是保证模块顺序加载和运行。下半部分是我们的JS代码,包裹了一个函数,也就是模块。运行的时候模块是作为Runtime的参数被传进去的。
(function(modules) {
// Runtime
})([
// 模块数组
])
1
2
3
4
5
2
3
4
5
webpack源码中,添加模块链的函数中,就在添加新的模块之前,先通过模块标识来判断是否存在,已经存在的模块不会重复添加到模块队列中。
_addModule(module, callback) {
const identifier = module.identifier();
const alreadyAddedModule = this._modules.get(identifier);
//根据模块标识,判断模块是否已经加载
if (alreadyAddedModule) {
return callback(null, alreadyAddedModule);
}
...
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
# 总结:对于多次引用的模块是不会重复加载的。
# 2. 模块循环引用问题如何处理?
理论上循环引用会导致栈溢出,但并非所有循环引用都会导致栈溢出。
# 案例一(不会栈溢出):
脚本文件a.js
代码如下。
exports.done = false;
var b = require('./b.js');
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
1
2
3
4
5
2
3
4
5
再看b.js
的代码。
exports.done = false;
var a = require('./a.js');
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
1
2
3
4
5
2
3
4
5
脚本main.js
var a = require('./a.js');
var b = require('./b.js');
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
1
2
3
2
3
编译结果:
(function (modules) {
var installedModules = {};
function __webpack_require__(moduleId) {
// 检查 installedModules 中是否存在对应的 module
// 如果存在就返回 module.exports
if (installedModules[moduleId]) {
return installedModules[moduleId].exports;
}
// 创建一个新的 module 对象,用于下面函数的调用
var module = installedModules[moduleId] = {
i: moduleId,
l: false,
exports: {}
};
// 从 modules 中找到对应的模块初始化函数并执行
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 标识 module 已被加载过
module.l = true;
return module.exports;
}
return __webpack_require__(__webpack_require__.s = "./src/index1.js");
})
({
"./src/a.js":
(function (module, exports, __webpack_require__) {
exports.done = false;
var b = __webpack_require__("./src/b.js");
console.log('在 a.js 之中,b.done = %j', b.done);
exports.done = true;
console.log('a.js 执行完毕');
}),
"./src/b.js":
(function (module, exports, __webpack_require__) {
exports.done = false;
var a = __webpack_require__("./src/a.js");
console.log('在 b.js 之中,a.done = %j', a.done);
exports.done = true;
console.log('b.js 执行完毕');
}),
"./src/index1.js":
(function (module, exports, __webpack_require__) {
debugger;
var a = __webpack_require__("./src/a.js");
var b = __webpack_require__("./src/b.js");
console.log('在 main.js 之中, a.done=%j, b.done=%j', a.done, b.done);
})
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# webpack_require 做了以下几件事:
- 根据 moduleId 查看 installedModules 中是否存在相应的 module ,如果存在就返回对应的 module.exports
- 如果 module 不存在,就创建一个新的 module 对象,并且使 installedModules[moduleId] 指向新建的 module 对象
- 根据 moduleId 从 modules 对象中找到对应的模块初始化函数并执行,依次传入 module,module.exports,webpack_require。可以看到,webpack_require 被当作参数传入,使得所有模块内部都可以通过调用该函数来引入其他模块
- 最后一步,返回 module.exports
# 看具体的代码逻辑:
a.js
脚本先输出一个done
变量,然后加载另一个脚本文件b.js;
b.js
执行到第二行,就会去加载a.js
,这时,就发生了“循环加载”。系统会去a.js
模块对应对象的exports
属性取值,可是因为a.js
还没有执行完,从exports
属性只能取回已经执行的部分,而不是最后的值。a.js
执行了一行,返回一个变量done,值为false;b.js
接着往下执行,等到全部执行完毕,再把执行权交还给a.js
a.js
接着往下执行,直到执行完毕。
运行结果为
在 b.js 之中,a.done = false
b.js 执行完毕
在 a.js 之中,b.done = true
a.js 执行完毕
在 main.js 之中, a.done=true, b.done=true
1
2
3
4
5
2
3
4
5
# 总结
- webpack 遇到模块循环引用时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异;
- webpack 的模块模式是基于CommonJS模式的,输入的是被输出值的拷贝,不是引用。
# 案例二(栈溢出):
a.js
import f from './b';
export default val => {
f('a');
console.log(val);
}
1
2
3
4
5
2
3
4
5
b.js
import f from './a';
export default val=>{
f('b');
console.log(val);
}
1
2
3
4
5
2
3
4
5
main.js
import a from './a';
a(1);
1
2
2
如果模块导出是函数,这种循环引用是会栈溢出的。
# 解决方案
不管是上面哪种情况,在webpack打包的时候,都是不会报错的。所以可以借助插件 circular-dependency-plugin来进行提示;
← 原理解析