基于本文回答

播面 播面

文解图解播客视频,多维讲透八股文
0
评论

当两个异步 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 正在加载中

流程控制(防止重复下载):

  1. 第一次请求(例如触发 Chunk A):

    • Chunk A 发现自己依赖公共 Chunk C。
    • Webpack 检查 installedChunks[C],发现是 undefined
    • Webpack 创建一个 Promise 和一个 <script> 标签去下载 Chunk C,并将 installedChunks[C] = [resolve, reject, promise](标记为“加载中”)。
  2. 第二次请求(例如几乎同时或随后触发 Chunk B):

    • Chunk B 也声明依赖公共 Chunk C。
    • Webpack 检查 installedChunks[C]
      • 如果还在下载中: 发现是一个 Promise,Webpack 会直接复用这个已经存在的 Promise不会重复创建 <script> 标签
      • 如果已经下载完成: 发现状态是 0,Webpack 会直接返回一个已决(Resolved)的 Promise。
  3. 只有当公共 Chunk C 的 Promise resolve 后,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;
}

流程控制(防止重复执行):

  1. Chunk A 第一次用到该公共模块(假设 ID 为 ./src/common.js)时,调用 __webpack_require__('./src/common.js')
  2. Webpack 发现 __webpack_module_cache__ 中没有该 ID,于是执行该模块的代码,并把导出的结果存入 __webpack_module_cache__['./src/common.js']
  3. Chunk B 也用到该模块时,再次调用 __webpack_require__('./src/common.js')
  4. Webpack 命中 if (cachedModule !== undefined) 条件,直接返回之前缓存的 exports,该模块的代码不会被第二次执行。

总结 (TL;DR)

阶段 机制/技术 解决的问题
1. 打包阶段 SplitChunksPlugin 将公共模块提取为独立 Chunk,避免代码体积冗余。
2. 网络加载 installedChunks (Promise 缓存) 确保同一个 Chunk 文件只发起一次网络请求(单例 Script 标签)。
3. 代码执行 __webpack_module_cache__ (Module 缓存) 确保同一个模块函数只被执行一次,之后调用直接获取缓存的 exports