当两个异步 Chunk 依赖同一个公共模块时,Webpack 是如何保证该模块只被加载和执行一次的?
在 Webpack 中,当两个异步 Chunk(通过 import() 动态导入)依赖同一个公共模块时,Webpack 通过 编译期提取 和 运行时双重缓存机制(网络加载缓存 + 模块执行缓存),来确保该公共模块只被下载一次,且只被执行一次。
具体的过程可以分为以下三个层面来解析:
1. 构建期:代码分割与依赖图提取 (SplitChunksPlugin)
在编译打包阶段,Webpack 会分析模块依赖图:
- 如果默认的
SplitChunksPlugin配置生效,Webpack 会识别出这个被多次引用的公共模块。 - Webpack 会将这个公共模块抽离成一个独立的 Chunk(公共 Chunk)。
- 此时,原有的两个异步 Chunk(Chunk A 和 Chunk B)不再直接包含该公共模块的代码,而是生成一个依赖清单,声明它们都依赖这个新的公共 Chunk。
2. 运行时网络层:Chunk 加载缓存 (__webpack_require__.e)
当页面在运行过程中触发异步加载时,Webpack 的运行时(Runtime)通过 __webpack_require__.e 函数来加载异步 Chunk 文件。
Webpack 在内存中维护了一个 installedChunks 对象,用来记录所有 Chunk 的加载状态:
0: 表示该 Chunk 已经加载完成。undefined: 表示该 Chunk 尚未加载。Promise: 表示该 Chunk 正在加载中。
流程控制(防止重复下载):
第一次请求(例如触发 Chunk A):
- Chunk A 发现自己依赖公共 Chunk C。
- Webpack 检查
installedChunks[C],发现是undefined。 - Webpack 创建一个
Promise和一个<script>标签去下载 Chunk C,并将installedChunks[C] = [resolve, reject, promise](标记为“加载中”)。
第二次请求(例如几乎同时或随后触发 Chunk B):
- Chunk B 也声明依赖公共 Chunk C。
- Webpack 检查
installedChunks[C]:- 如果还在下载中: 发现是一个
Promise,Webpack 会直接复用这个已经存在的Promise,不会重复创建<script>标签。 - 如果已经下载完成: 发现状态是
0,Webpack 会直接返回一个已决(Resolved)的 Promise。
- 如果还在下载中: 发现是一个
只有当公共 Chunk C 的
Promiseresolve 后,Chunk A 和 Chunk B 的业务代码才会继续向下执行。
3. 运行时执行层:模块执行缓存 (__webpack_require__)
即使脚本文件只下载了一次,当两个 Chunk 去调用这个公共模块时,如何保证模块内部的代码(比如全局副作用、初始化逻辑)只执行(Run)一次?
Webpack 在 Runtime 中维护了一个全局的 __webpack_module_cache__(模块导出结果缓存对象)。
其底层逻辑简化后如下:
javascript
// Webpack 内部的模块加载与执行函数
function __webpack_require__(moduleId) {
// 1. 检查缓存:如果该模块已经执行过,直接返回之前缓存的 exports 对象
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 2. 初始化模块对象,并写入缓存
var module = (__webpack_module_cache__[moduleId] = {
id: moduleId,
loaded: false,
exports: {},
});
// 3. 执行模块代码(仅在第一次被 require 时执行)
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 4. 标记为已加载,并返回 exports
module.loaded = true;
return module.exports;
}
流程控制(防止重复执行):
- 当 Chunk A 第一次用到该公共模块(假设 ID 为
./src/common.js)时,调用__webpack_require__('./src/common.js')。 - Webpack 发现
__webpack_module_cache__中没有该 ID,于是执行该模块的代码,并把导出的结果存入__webpack_module_cache__['./src/common.js']。 - 当 Chunk B 也用到该模块时,再次调用
__webpack_require__('./src/common.js')。 - Webpack 命中
if (cachedModule !== undefined)条件,直接返回之前缓存的exports,该模块的代码不会被第二次执行。
总结 (TL;DR)
| 阶段 | 机制/技术 | 解决的问题 |
|---|---|---|
| 1. 打包阶段 | SplitChunksPlugin |
将公共模块提取为独立 Chunk,避免代码体积冗余。 |
| 2. 网络加载 | installedChunks (Promise 缓存) |
确保同一个 Chunk 文件只发起一次网络请求(单例 Script 标签)。 |
| 3. 代码执行 | __webpack_module_cache__ (Module 缓存) |
确保同一个模块函数只被执行一次,之后调用直接获取缓存的 exports。 |