# 实战解析

# 1. 多次引用的模块是如何处理的?会重复加载嘛?

不会,这个问题主要是涉及webpack的模块化机制。 首先看个案例,a.js

export default function(){
    console.log('a');
}
1
2
3

b.js

import a from './a';
export default function(){
    console.log('我是b');
    a();
}
1
2
3
4
5

入口文件index.js

import a from './a';
import b from './b';
a();
b();
1
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

webpack在解析入口文件时,会通过入口文件来逐步分析模块依赖。最后生成的模板是一个立即执行包裹的函数,重新定义了__webpack_require__方法,模块依赖会按照各自标识的方式作为参数传递给函数。 上面编译出来的代码主要包含两个部分:Runtime,模块。上半部分就是Runtime,作用是保证模块顺序加载和运行。下半部分是我们的JS代码,包裹了一个函数,也就是模块。运行的时候模块是作为Runtime的参数被传进去的。

(function(modules) {
    // Runtime
})([
    // 模块数组
])
1
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. 模块循环引用问题如何处理?

理论上循环引用会导致栈溢出,但并非所有循环引用都会导致栈溢出。

# 案例一(不会栈溢出):

脚本文件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

再看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

脚本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

编译结果:

(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

# webpack_require 做了以下几件事:

  1. 根据 moduleId 查看 installedModules 中是否存在相应的 module ,如果存在就返回对应的 module.exports
  2. 如果 module 不存在,就创建一个新的 module 对象,并且使 installedModules[moduleId] 指向新建的 module 对象
  3. 根据 moduleId 从 modules 对象中找到对应的模块初始化函数并执行,依次传入 module,module.exports,webpack_require。可以看到,webpack_require 被当作参数传入,使得所有模块内部都可以通过调用该函数来引入其他模块
  4. 最后一步,返回 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

# 总结

  • webpack 遇到模块循环引用时,返回的是当前已经执行的部分的值,而不是代码全部执行后的值,两者可能会有差异;
  • webpack 的模块模式是基于CommonJS模式的,输入的是被输出值的拷贝,不是引用。

# 案例二(栈溢出):

a.js

import f from './b';
export default val => {
    f('a');
    console.log(val);
}
1
2
3
4
5

b.js

import f from './a';
export default val=>{
    f('b');
    console.log(val);
}
1
2
3
4
5

main.js

import a from './a';
a(1);
1
2

如果模块导出是函数,这种循环引用是会栈溢出的。

# 解决方案

不管是上面哪种情况,在webpack打包的时候,都是不会报错的。所以可以借助插件 circular-dependency-plugin来进行提示;

Last Updated: 10/29/2020, 2:17:25 AM