Roger Leung‘s Epcot

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021
Roger Leung‘s Epcot

Choose mode

  • dark
  • auto
  • light
Blog
Note
Github (opens new window)
author-avatar

Roger Leung ( z3rog )

18

Article

20

Tag

Blog
Note
Github (opens new window)
  • 首页
  • 框架与工具链

    • Vue 3
    • Vue 2
    • Webpack 4
  • 前端性能优化

    • 性能优化的必要性
    • 性能指标
    • 基本手段
    • 离线缓存
  • 浏览器机制

    • 架构
    • 导航
    • 渲染机制
    • 缓存机制
  • 网络协议

    • TCP 协议
    • HTTP 协议
    • HTTPS 协议
    • HTTP 2 协议
    • HTTP 3 协议
  • 其他

    • V8 中的快慢属性与快慢数组
    • V8 解析执行 JavaScript 流程简述
    • V8 的垃圾回收机制
    • 100 行代码写一个破产版 Vite
    • 浅谈微前端

Webpack 4

vuePress-theme-reco Roger Leung ( z3rog )    2018 - 2021

Webpack 4

Roger Leung ( z3rog ) 2020-06-19 Webpack

这篇笔记的内容所涉及的知识范围比较大,一时之间想不到如何很好地组织,甚至部分话题也许更适合单独拆开一篇文章来讲,所以下面的内容可能会随时进行拆分重新组织,不定期保持更新

# 运行原理

Webpack 运行时大致分为以下几个步骤:

  • 读取 webpack.config.js 配置文件,将文件 export 的配置 options 拿到后,与默认的配置做合并,然后 new Compiler()。若识别到传递的 options 是数组,则调用 new MultiCompiler()
  • compiler 实例根据配置实例化一个 compilation,用于表示 compiler 的当次编译上下文
  • 根据配置的文件入口开始分析文件的依赖,并将内容传递给各种 loader,最终生成文件内容的 AST
  • 从入口开始递归上述步骤
  • 遍历所有的 chunk 及各 chunk 的依赖,调用代码模版的生成函数,输出最终的 JavaScript 文件

小知识

我们熟知的 webpack-dev-server 实际上使用了 chokidar (opens new window) 来监听所有文件的修改时间,在同一 compiler 实例下实例化新的 compilation。当然,它内部还会做一些

# CommonJS

CommonJS 是以在浏览器环境之外构建JavaScript 生态系统为目标而产生的项目

众所周知,Node 是 CommonJS 规范的践行者之一,在编写 Node 代码中可能有大量按需 require 的代码,比如:

let module
if (/* some condition*/) {
    module = require('./moduleA')
} else {
    module = require('./moduleB)
}
1
2
3
4
5
6

CommonJS 是同步加载的,这套规范在 Node 端可行的原因之一是硬盘/内存的读取远远高于网络 I/O,Node 加载模块是不需要发送网络请求的,所以上面代码在需要的时候再对 moduleA 或 moduleB 进行引入,在时间效率上不会太大影响。但浏览器端如果同步加载一个很大的文件,网络开销造成的页面阻塞会极大降低用户体验。那,Webpack 是如何实现所谓的 CommonJS 规范的?这就比较有意思了。

# Webpack 的 CommonJS

假设我们使用 Webpack 来打包以下两个简单文件:

webpack-basic-demo

Webpack 打包后的将近 100 行代码见下:

(function (modules) { // webpackBootstrap
  // The module cache
  var installedModules = {};

  // The require function
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    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: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }


  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // define __esModule on exports
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { return value[key]; }.bind(null, key));
    return ns;
  };

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function (module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // __webpack_public_path__
  __webpack_require__.p = "";


  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/data.js":
    (function (module, __webpack_exports__, __webpack_require__) {

      "use strict";
      eval("__webpack_require__.r(__webpack_exports__);\nconst result = '我是文件2';\n/* harmony default export */ __webpack_exports__[\"default\"] = (result);\n\n\n//# sourceURL=webpack:///./src/data.js?");
    }),
  "./src/index.js":
    (function (module, __webpack_exports__, __webpack_require__) {

      "use strict";
      eval("__webpack_require__.r(__webpack_exports__);\n/* harmony import */ var _data_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./data.js */ \"./src/data.js\");\n\nconst result = 'Roger Leung';\nconsole.log(result);\nconsole.log('----------');\nconsole.log(_data_js__WEBPACK_IMPORTED_MODULE_0__[\"default\"]);\n\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
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99

我们来一步步拆解这些代码的各个部分。

# 基本代码结构

(function(modules) {
    // ...
})({
    './src/index.js': (function(module, __webpack_exports__, __webpack_require__) {
        'use strict'
        // ...
    }),
    './src/data.js': , // same as above
    // moduleKey3 or more
})
1
2
3
4
5
6
7
8
9
10

可以看到,Webpack 打包后的文件是一个 IIFE,其中传入的 modules 参数是一个包含所有文件的一维拍平后的 key-value 对象,key 对应的是文件的路径,value 则是一个接受三个参数的函数,我们在对应文件下编写的代码,都被包装到这个函数下了。

现在需要看的是,匿名函数接收 modules 参数后,做了些什么事情。

# __webpack_require__

匿名函数的函数体内,定义了一个 __webpack_require__ 函数,此处摘取下来:

var installedModules = {};

function __webpack_require__(moduleId) {
    if (installedModules[moduleId]) {
        return installedModules[moduleId].exports;
    }

    var module = installedModules[moduleId] = {
        i: moduleId,
        l: false,
        exports: {}
  };

  modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

  module.l = true;

  return module.exports;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

函数体内首先定义了一个全局的 installedModules 对象,用于将所有已加载的模块缓存下来。所以,__webpack_require__ 方法的开头,先检查当前 require 的模块是否已加载过:

// 如果 installedModules 中已设置过对应 moduleId 的属性
// 则直接返回该属性值下的 exports 对象
if (installedModules[moduleId]) {
    return installedModules[moduleId].exports;
}
1
2
3
4
5

那这个 exports 对象是什么呢?

// 如果模块未被导入过,则构造一个新的 module 对象,该对象有三个属性
var module = installedModules[moduleId] = {
    // 模块的 id,设置为传入的 moduleId,development 模式下为文件路径
    i: moduleId,
    // 模块对应的函数是否已导入
    l: false,
    // 构造的 exports 对象
    exports: {}
};
// 构造完 module 对象后,需要将模块对应的 function 执行一遍
// modules[moduleId] 是对应的 function
// 并传入 module、module.exports 及 __webpack_require_ 方法
modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
// 模块的函数执行完毕,设置标记 l 为 true,表示已导入完成
module.l = true;
// 最后返回该模块的 exports 属性
return module.exports;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

现在假设 Webpack 要执行我们的入口文件 ./src/index.js(后面会讲到真正在哪里开始执行),按照前面的分析,./src/index.js 会被作为 key 传入到 __webpack_require__ 中。这时 Webpack 的 installModules 中仍没有该 key 的属性,则继续往下走,最终通过 modules[moduleId] 获取到模块的函数:


const modules = {
    './src/index.js': (function(module, __webpack_exports__, __webpack_require__) {
        'use strict'
        // ...
    }),
    // ...
}
const moduleId = './src/index.js'

// so
modules[moduleId] === (function(module, __webpack_exports__, __webpack_require__) {
    'use strict'
    // ...
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

调用 modules[moduleId].call(module.exports, module, module.exports, __webpack_require__ 则相当于将 module、module.exports、__webpack_require__ 当作实参传到该入口模块的函数中了。

那到底,Webpack 是在哪里执行入口文件的 require 的?是的,在匿名函数的函数体最后一行:


// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
1
2
3

除了将入口文件的 moduleId 挂到 __webpack_require__.s 上,还一并执行了一次该方法。

之前的代码,更多是在为 __webpack_require__ 上挂载一些方法。

比如:

  • __webpack_require__.r: 为 module.exports 对象上定义 __esModule 及 Symbol.toStringTag 属性(自行参考阅读上面代码去理解)
  • __webpack_require__.p:定义 webpack public path
  • __webpack_require__.o:一个 Object.prototype.hasOwnProperty.call 的封装,用于检查对象上是否已有某属性
  • __webpack_require__.d:为 exports 对象上特定的属性定义对应的 getter
  • etc...

# webpackJsonp

前面举例的是一个最基本的 Webpack Demo。现在我们来把问题稍微弄复杂一点:









 






// original index.js
import data from './data.js';
const result = 'Roger Leung';
console.log(result);
console.log('----------');
console.log(data);

// change to 
import('./data.js').then(data => {
    const result = 'Roger Leung';
    console.log(result);
    console.log('----------');
    console.log(data);
});
1
2
3
4
5
6
7
8
9
10
11
12
13
14

当我们使用动态的 import() 函数后,Webpack 生成的代码发生了巨大变化,多了一个 0.js 并且 main.js 的主匿名函数下多了很多原来没见过的代码(可快速浏览跳过):

// main.js
(function (modules) { // webpackBootstrap
  // install a JSONP callback for chunk loading
  function webpackJsonpCallback(data) {
    var chunkIds = data[0];
    var moreModules = data[1];


    // add "moreModules" to the modules object,
    // then flag all "chunkIds" as loaded and fire callback
    var moduleId, chunkId, i = 0, resolves = [];
    for (; i < chunkIds.length; i++) {
      chunkId = chunkIds[i];
      if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
        resolves.push(installedChunks[chunkId][0]);
      }
      installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
      if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
        modules[moduleId] = moreModules[moduleId];
      }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);

    while (resolves.length) {
      resolves.shift()();
    }

  };


  // The module cache
  var installedModules = {};

  // object to store loaded and loading chunks
  // undefined = chunk not loaded, null = chunk preloaded/prefetched
  // Promise = chunk loading, 0 = chunk loaded
  var installedChunks = {
    "main": 0
  };



  // script path function
  function jsonpScriptSrc(chunkId) {
    return __webpack_require__.p + "" + ({}[chunkId] || chunkId) + ".js"
  }

  // The require function
  function __webpack_require__(moduleId) {

    // Check if module is in cache
    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: {}
    };

    // Execute the module function
    modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);

    // Flag the module as loaded
    module.l = true;

    // Return the exports of the module
    return module.exports;
  }

  // This file contains only the entry chunk.
  // The chunk loading function for additional chunks
  __webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];


    // JSONP chunk loading for javascript

    var installedChunkData = installedChunks[chunkId];
    if (installedChunkData !== 0) { // 0 means "already installed".

      // a Promise means "currently loading".
      if (installedChunkData) {
        promises.push(installedChunkData[2]);
      } else {
        // setup Promise in chunk cache
        var promise = new Promise(function (resolve, reject) {
          installedChunkData = installedChunks[chunkId] = [resolve, reject];
        });
        promises.push(installedChunkData[2] = promise);

        // start chunk loading
        var script = document.createElement('script');
        var onScriptComplete;

        script.charset = 'utf-8';
        script.timeout = 120;
        if (__webpack_require__.nc) {
          script.setAttribute("nonce", __webpack_require__.nc);
        }
        script.src = jsonpScriptSrc(chunkId);

        // create error before stack unwound to get useful stacktrace later
        var error = new Error();
        onScriptComplete = function (event) {
          // avoid mem leaks in IE.
          script.onerror = script.onload = null;
          clearTimeout(timeout);
          var chunk = installedChunks[chunkId];
          if (chunk !== 0) {
            if (chunk) {
              var errorType = event && (event.type === 'load' ? 'missing' : event.type);
              var realSrc = event && event.target && event.target.src;
              error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
              error.name = 'ChunkLoadError';
              error.type = errorType;
              error.request = realSrc;
              chunk[1](error);
            }
            installedChunks[chunkId] = undefined;
          }
        };
        var timeout = setTimeout(function () {
          onScriptComplete({ type: 'timeout', target: script });
        }, 120000);
        script.onerror = script.onload = onScriptComplete;
        document.head.appendChild(script);
      }
    }
    return Promise.all(promises);
  };

  // expose the modules object (__webpack_modules__)
  __webpack_require__.m = modules;

  // expose the module cache
  __webpack_require__.c = installedModules;

  // define getter function for harmony exports
  __webpack_require__.d = function (exports, name, getter) {
    if (!__webpack_require__.o(exports, name)) {
      Object.defineProperty(exports, name, { enumerable: true, get: getter });
    }
  };

  // define __esModule on exports
  __webpack_require__.r = function (exports) {
    if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
      Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
    }
    Object.defineProperty(exports, '__esModule', { value: true });
  };

  // create a fake namespace object
  // mode & 1: value is a module id, require it
  // mode & 2: merge all properties of value into the ns
  // mode & 4: return value when already ns object
  // mode & 8|1: behave like require
  __webpack_require__.t = function (value, mode) {
    if (mode & 1) value = __webpack_require__(value);
    if (mode & 8) return value;
    if ((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
    var ns = Object.create(null);
    __webpack_require__.r(ns);
    Object.defineProperty(ns, 'default', { enumerable: true, value: value });
    if (mode & 2 && typeof value != 'string') for (var key in value) __webpack_require__.d(ns, key, function (key) { return value[key]; }.bind(null, key));
    return ns;
  };

  // getDefaultExport function for compatibility with non-harmony modules
  __webpack_require__.n = function (module) {
    var getter = module && module.__esModule ?
      function getDefault() { return module['default']; } :
      function getModuleExports() { return module; };
    __webpack_require__.d(getter, 'a', getter);
    return getter;
  };

  // Object.prototype.hasOwnProperty.call
  __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };

  // __webpack_public_path__
  __webpack_require__.p = "";

  // on error function for async loading
  __webpack_require__.oe = function (err) { console.error(err); throw err; };

  var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
  var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
  jsonpArray.push = webpackJsonpCallback;
  jsonpArray = jsonpArray.slice();
  for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
  var parentJsonpFunction = oldJsonpFunction;


  // Load entry module and return exports
  return __webpack_require__(__webpack_require__.s = "./src/index.js");
})({
  "./src/index.js":
    (function (module, exports, __webpack_require__) {

      eval("__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./data */ \"./src/data.js\")).then(data => {\n    const result = 'Roger Leung';\n    console.log(result);\n    console.log('----------');\n    console.log(data);\n})\n\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
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
// 0.js
(window["webpackJsonp"] = window["webpackJsonp"] || []).push([[0], {
  "./src/data.js":
    (function (module, __webpack_exports__, __webpack_require__) {
      "use strict";
      eval("__webpack_require__.r(__webpack_exports__);\nconst result = '我是文件2';\n/* harmony default export */ __webpack_exports__[\"default\"] = (result);\n\n\n//# sourceURL=webpack:///./src/data.js?");
    })
}]);
1
2
3
4
5
6
7
8

src/index.js 下动态引入的 data.js 被打包成一个独立的 chunk:0.js,并且显而易见的是 window 上多了一个名为 webpackJsonp 的属性,从赋值逻辑上看他应该是一个数组。0.js 所做的事情十分简单:向 window.webpackJsonp 上新增一个数组,数组的第二项内容为 modules 对象,格式与前文描述过的一样,第一项也为一个数组,在此暂且猜测是 chunk 的名字。

为了搞清楚 main.js 中的 IIFE 函数体中新增的代码都做了些什么,我们跳过各种函数的定义,从函数执行的角度一步步分析:

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;

// Load entry module and return exports
return __webpack_require__(__webpack_require__.s = "./src/index.js");
1
2
3
4
5
6
7
8
9

上面这一段代码在初次执行时只会导入入口文件,显而易见 webpackJsonp 数组暂时为空。所以会先执行 './src/index.js' 对应的模块函数,即:

(function (module, exports, __webpack_require__) {

    eval("__webpack_require__.e(/*! import() */ 0).then(__webpack_require__.bind(null, /*! ./data */ \"./src/data.js\")).then(data => {\n    const result = 'Roger Leung';\n    console.log(result);\n    console.log('----------');\n    console.log(data);\n})\n\n\n//# sourceURL=webpack:///./src/index.js?");
})(/* 省略 */)
1
2
3
4

eval 函数中执行的 __webpack_require__.e 正是编译后用来取代 import() 的方法。具体来看下它的定义:

// 与前面一样,主函数必定有一个全局定义的 installedModules 对象
var installedModules = {}

/**
 * 定义导入中或已导入完毕的 chunks,其中:
 * undefined:  chunk 未被导入
 * null:       chunk 被 preload 或 prefetch
 * Promise:    chunk 正在导入,当且仅当这种情况 installedChunks[chunkId]) 才会为 true
 * 0:           chunk 已被导入完毕
 */
var installedChunks = {
    'main': 0
}

// ... 省略中间代码

// The chunk loading function for additional chunks
__webpack_require__.e = function requireEnsure(chunkId) {
    var promises = [];
    var installedChunkData = installedChunks[chunkId];
    if (installedChunkData !== 0) {
        if (installedChunkData) {
            // 如果 installedChunkData 为 true,获取其 Promise 的值并追加至 promises 数组
            promises.push(installedChunkData[2]);
        } else {
            // 否则,设置 installedChunks[chunkId] = [resolve, reject, promise]
            var promise = new Promise(function (resolve, reject) {
                installedChunkData = installedChunks[chunkId] = [resolve, reject];
            });
            promises.push(installedChunkData[2] = promise);

            // start chunk loading
            var script = document.createElement('script');
            var onScriptComplete;

            script.charset = 'utf-8';
            script.timeout = 120;
            if (__webpack_require__.nc) {
                script.setAttribute("nonce", __webpack_require__.nc);
            }
            script.src = jsonpScriptSrc(chunkId);

            // create error before stack unwound to get useful stacktrace later
            var error = new Error();
            onScriptComplete = function (event) {
                // avoid mem leaks in IE.
                script.onerror = script.onload = null;
                clearTimeout(timeout);
                var chunk = installedChunks[chunkId];
                // 若脚本导入完毕会调用 webpackJsonpCallback 将 chunk 置为 0 ,后文详述
                if (chunk !== 0) {
                    if (chunk) {
                        var errorType = event && (event.type === 'load' ? 'missing' : event.type);
                        var realSrc = event && event.target && event.target.src;
                        error.message = 'Loading chunk ' + chunkId + ' failed.\n(' + errorType + ': ' + realSrc + ')';
                        error.name = 'ChunkLoadError';
                        error.type = errorType;
                        error.request = realSrc;
                        chunk[1](error);        // chunk[1] 是前面 new Promise 时的 reject 回调
                    }
                    installedChunks[chunkId] = undefined;
                }
            };
            var timeout = setTimeout(function () {
                onScriptComplete({ type: 'timeout', target: script });
            }, 120000);
            script.onerror = script.onload = onScriptComplete;
            document.head.appendChild(script);
        }
    }
    return Promise.all(promises);
};
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72

可以看到,requireEnsure(亦即__webpack_require__.e) 的核心逻辑是用来导入各种 chunk。以我们的例子展开,它的执行顺序是这样的:

  • __webpack_require__(__webpack_require__.s = "./src/index.js") 时调用 __webpack_require__.e(0) 导入 chunk 0.js
  • 调用过程中,首先检查 0.js 是否已经被导入过,判断为否
  • 在 installedChunks 对象中标记 '0': [resolve, reject],然后创建一个 script 标签尝试加载 0.js
  • 若 0.js 导入成功,则 0.js 中的 IIFE 必定会调用会将 0.js 的 modules 及其 modules function 追加到 window.webpackJsonp 中
  • 注意一个细节,requireEnsure 中 onScriptComplete 并没有处理 promise resolve。它是通过 eval("webpack_require.e(0).then(__webpack_require__.bind(null,"./src/data.js")).then(...); 这一行,重新执行了一次 __webpack_require__ 来 resolve 的(怎么 resolve 继续看后文)
  • 当再次 __webpack_require__ 时,前面获取 window.webpackJsonp 的数组就不再为空了,会走到 webpackJsonpCallback 方法中。

我们将前面加载入口文件时跳过的代码再次摘取至此:

var jsonpArray = window["webpackJsonp"] = window["webpackJsonp"] || [];
var oldJsonpFunction = jsonpArray.push.bind(jsonpArray);
jsonpArray.push = webpackJsonpCallback;
jsonpArray = jsonpArray.slice();
for (var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);
var parentJsonpFunction = oldJsonpFunction;
1
2
3
4
5
6

获取 window.webpackJsonp 数组后对该数组遍历,并将数组每一项以此传入 webpackJsonpCallback 调用。以刚刚 0.js 的 IIFE 代码为例,是不是相当于将 [[0], { ...modules }] 这个数组传入到 webpackJsonpCallback ?假设当前传入的就是这样的数据,我们来分析下 webpackJsonpCallback:

function webpackJsonpCallback(data) { 
    var chunkIds = data[0];     // chunkIds = [0]  
    var moreModules = data[1];  // moreModules = { './src/data.js': (function(...) {})}

    var moduleId, chunkId, i = 0, resolves = [];
    for (; i < chunkIds.length; i++) {
        // 遍历 chunkIds,当前只有 id 为 0 的 chunk,即 0.js
        chunkId = chunkIds[i];
        // 0.js 未被导入 installedChunks 中
        if (Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {
            // 如果进入该 block,说明 0.js 之前被某个 chunk 尝试导入过,但未导入完毕
            // installedChunks[chunkId][0],这是导入 0.js 时实例化的 promise 的 resolve 函数
            resolves.push(installedChunks[chunkId][0]);
        }
        // 这时可以将 0.js 标志为已导入
        installedChunks[chunkId] = 0;
    }
    for (moduleId in moreModules) {
        // 将新传入的所有 module 追加至 modules 上
        if (Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {
            modules[moduleId] = moreModules[moduleId];
        }
    }
    if (parentJsonpFunction) parentJsonpFunction(data);

    while (resolves.length) {
        resolves.shift()();
    }
};
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

至此,Webpack 基本的同步异步 import 流程都已讲解完毕。

# Tree-shaking

TO BE CONTINUED