webpack 4 源码主流程分析(八):生成 chunk

module 构建完成后,回到文件 Compiler.js 的 compile 的 make 钩子的回调里:

this.hooks.make.callAsync(compilation, (err) => {
  //...
  compilation.finish((err) => {
    //...
    compilation.seal((err) => {
      //...this.hooks.afterCompile.callAsync(compilation, (err) => {
        //...returncallback(null, compilation);
      });
    });
  });
});
复制代码

compilation.finish & compilation.seal

执行 compilation.finish,触发 compilation.hooks:finishModules,执行插件 FlagDependencyExportsPlugin 注册的事件,作用是遍历所有 module 将 export 出来的变量以数组的形式,单独存储到 module.buildMeta.providedExports变量下。

然后执行 reportDependencyErrorsAndWarnings 收集生成每一个 module 时暴露出来的 err 和 warning。

然后走回调执行 compilation.seal 触发了海量 hooks,为我们侵入 webpack 构建流程提供了海量钩子。我们略过本 demo 没有注册方法的钩子,执行:

this.hooks.seal.call();
复制代码

触发插件 WarnCaseSensitiveModulesPlugin:模块文件路径需要区分大小写的警告

this.hooks.optimizeDependencies.call(this.modules);
复制代码

production 模式会触发插件:

  • SideEffectsFlagPlugin:识别 package.json 或者 module.rules 的 sideEffects 标志(纯的 ES2015 模块),安全地删除未用到的 export 导出

  • FlagDependencyUsagePlugin:编译时标记依赖 unused harmony export 用于 Tree shaking

chunk 初始化

在触发 compilation.hooks:beforeChunks 后,开始遍历入口对象 this._preparedEntrypoints,为每一个入口生成一个 chunk:

const chunk = this.addChunk(name);
复制代码

该方法里做了缓存判断后执行 new Chunk(name),并同时添加 chunk 到 Compilation.chunks,继续执行:

const entrypoint = newEntrypoint(name);
复制代码

Entrypoint 类扩展于 ChunkGroup 类,是 chunks 的集合,主要用来优化 chunk graph。

继续执行设置了 Compilation.runtimeChunk & Compilation.namedChunkGroups & Compilation.entrypoints & Compilation.chunkGroups 和 ChunkGroup.origins,然后执行:

GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);
复制代码

建立了 chunk 与 entrypoint,chunk 与 module 之间的联系,然后执行:

this.assignDepth(module);
复制代码

根据各个模块依赖的深度(多次依赖取最小值)设置 module.depth,入口模块则为 depth = 0。

遍历完 this._preparedEntrypoints 后,然后执行:

生成 chunk graph

buildChunkGraph(this, /** @type {Entrypoint[]} */ (this.chunkGroups.slice()));
复制代码

buildChunkGraph 用于生成并优化 chunk 依赖图,建立起各模块之前的关系。 分为三阶段:

// PART ONEvisitModules(compilation, inputChunkGroups, chunkGroupInfoMap, chunkDependencies, blocksWithNestedBlocks, allCreatedChunkGroups);
// PART TWOconnectChunkGroups(blocksWithNestedBlocks, chunkDependencies, chunkGroupInfoMap);
// Cleaup workcleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
复制代码

第一阶段

第一阶段主要建立了 chunkGroup,chunk,module(包括同步异步)之间的从属关系。

先执行:

const blockInfoMap = extraceBlockInfoMap(compilation);
复制代码

得到一个 map 结构: module 与该 module 内导入其他模块的关系,同步存入 modules,异步存入 blocks。以 demo 为例,得到 blockInfoMap:

{
  //...map结构0:{
    key:NormalModule, //avalue:{
      blocks:[ImportDependenciesBlock],//异步modules:[NormalModule] //b  modules为set结构
    }
  },
  1:{
    key: ImportDependenciesBlock,
    value:{
      blocks: [],
      modules:[NormalModule] //c
    }
  }
  2:{
    key: NormalModule, //cvalue:{
      blocks: [ImportDependenciesBlock],
      modules:[NormalModule] //d
    }
  }
  //........
}
复制代码

继续执行,设置了 queue 数组,push 入口 module 和对应的 action 等信息组成的对象,用于 while 循环:

queue.push({
  action: ENTER_MODULE,
  block: module,
  module,
  chunk,
  chunkGroup,
});
复制代码

设置了 chunkGroupInfoMap,他映射了 chunkGroup 和与他相关的信息对象:

chunkGroupInfoMap.set(chunkGroup, {
  chunkGroup,
  minAvailableModules: newSet(),
  minAvailableModulesOwned: true,
  availableModulesToBeMerged: [],
  skippedItems: [],
  resultingAvailableModules: undefined,
  children: undefined,
});
复制代码

然后执行:

while (queue.length) {
  //...while (queue.length) {
    //...if (chunkGroup !== queueItem.chunkGroup) {
      // 重置更新chunkGroup
    }
    switch (queueItem.action) {
      caseADD_AND_ENTER_MODULE: {
        // 如果 queueItem.module 在 minAvailableModules,则将该 queueItem 存入 skippedItems// 建立 chunk 和 module 之间的联系,将依赖的 module 存入该 chunk 里
      }
      caseENTER_MODULE: {
        // 设置 chunkGroup._moduleIndices 和 module.index,然后 queue.push 一个新的该 module 的 queueItem,action 设为 LEAVE_MODULE
      }
      casePROCESS_BLOCK: {
        // 0. 通过上文 blockInfoMap 映射关系及入口 module,找到入口 module 所依赖的其他同步、异步模块// 1. 遍历其同步模块 modules,如果对应 chunk 已有此 module 则跳过;判断此 module 是否在 minAvailableModules,有则将一个新的 queueItem 存入 skippedItems,没有则存入 queue,其中 queueItem.action 都设为 ADD_AND_ENTER_MODULE// 2. 遍历 blockInfoMap 里的异步模块 blocks// 2.1 创建一个对应异步依赖的 chunkGroup 和 chunk,并建立两者的联系,然后更新了 compilation.chunkGroups 和 compilation.namedChunkGroups,chunkGroupCounters(计数 map),blockChunkGroups(映射依赖和 ChunkGroup 的关系 map),allCreatedChunkGroups(收集被创建的ChunkGroup set)// 2.2 更新 chunkDependencies(map) 建立前一个 ChunkGroup 与新的 ChunkGroup 和 import 依赖的映射// 2.3 更新 queueConnect(map) 建立前一个 ChunkGroup 与新的 ChunkGroup 的映射// 2.4 更新 queueDelayed,同 queue,注意 module 是前一个的 module,action 为 PROCESS_BLOCK
      }
      caseLEAVE_MODULE: {
        // 设置 chunkGroup._moduleIndices2 和 module.index2
      }
    }
  }
  // 上文 while (queue.length) 从 enter module 开始,循环将所有同步依赖都加入到同一个 chunk 里,将 enter module 及它的同步依赖里的异步依赖都各自新建了chunkGroup 和 chunk,并将异步模块存入 queueDelayed,异步依赖中的异步依赖还未处理。while (queueConnect.size > 0) {
    // 计算可用的模块// 1. 在 chunkGroupInfoMap 中设置前一个 ChunkGroup 的信息对象的 resultingAvailableModules, children// 2. 在 chunkGroupInfoMap 中初始化新的 ChunkGroup 与他相关的信息对象的映射并设置了 availableModulesToBeMergedif (outdatedChunkGroupInfo.size > 0) {
      // 合并可用模块// 1.获取设置新的 ChunkGroup 信息对象的 minAvailableModules// 2.如果新的 ChunkGroup 信息对象的 skippedItems 不为空则 push 到 queue// 3.如果新的 ChunkGroup 信息对象的 children 不为空,则更新 queueConnect 递归循环
    }
  }
  // 把 queueDelayed 放入 queue 走 while 的最外层循环,目的的所有同步循环处理完后,然后才处理异步 module,如果异步 module 里还有异步 module,将放到下一次的 queue 走 while 的最外层循环if (queue.length === 0) {
    const tempQueue = queue; // ImportDependenciesBlock
    queue = queueDelayed.reverse();
    queueDelayed = tempQueue;
  }
}
复制代码
  • 在内部 while 对 queue.length 循环里( while+push 防递归爆栈,后序深度优先),从入口 module 开始,解析了所有同步 module 并建立了 module 与 chunk 的联系;解析了所有第一层异步(即非嵌套异步模块)的 module,并为每个不同的异步 mudule 都新建了 chunkGroup 和 chunk 并建立了两者的联系。

  • 然后在 while 对 queueConnect.size 的循环里,更新了 chunkGroupInfoMap 中前一个 ChunkGroup 的信息对象和初始化了新的 ChunkGroup 的信息对象,并获取了最小可用 module。

  • 同步 module 循环处理结束后,开始处理异步 module,将 queueDelayed 赋给 queue,走外部 while 对 queue.length 的循环。

  • 处理异步模块的时候,queue 里的 block 为 ImportDependenciesBlock 依赖,然后更新 chunkGroup 后, switch 走 PROCESS_BLOCK 获得本次异步对应的真正模块,后面的处理数据都将在新的 ChunkGroup 信息对象上。就这样循环处理,最终得到一个 Map 结构的 chunkGroupInfoMap。以本 demo 为例,得到:

{
  //...map结构0:{
    key:Entrypoint, //groupDebugId:5000value:{
      availableModulesToBeMerged:Array(0)
      children:Set(1) {} //ChunkGroup 5001chunkGroup:EntrypointminAvailableModules:Set(0)
      minAvailableModulesOwned:trueresultingAvailableModules:Set(3)
      skippedItems:Array(0)
    }
  },
  1:{
    key: ChunkGroup, //groupDebugId:5001value:{
      availableModulesToBeMerged:Array(0)
      children:Set(1) {} //ChunkGroup 5002chunkGroup:EntrypointminAvailableModules:Set(3)
      minAvailableModulesOwned:trueresultingAvailableModules:Set(5)
      skippedItems:Array(0)
    }
  }
  2:{
    key: ChunkGroup, //groupDebugId:5002value:{
      availableModulesToBeMerged:Array(0)
      children:undefinedchunkGroup:EntrypointminAvailableModules:Set(5)
      minAvailableModulesOwned:trueresultingAvailableModules:undefinedskippedItems:Array(1)
    }
  }
}
复制代码

第二阶段

第二阶段主要根据 ImportDependenciesBlock 建立了不同 chunkGroup 之间的父子关系。

遍历 chunkDependencies,chunkDependencies 是 Map 结构,保存着前一个 ChunkGroup 与新的 ChunkGroup 和 import 依赖之间的映射:

{
  //...map结构0:{
    key:Entrypoint, //groupDebugId:5000value:[
      {
        block:ImportDependenciesBlock,
        chunkGroup:ChunkGroup//groupDebugId:5001
      }
    ]
  },
  1:{
    key:ChunkGroup, //groupDebugId:5001value:[
      {
        block:ImportDependenciesBlock,
        chunkGroup:ChunkGroup//groupDebugId:5002
      }
    ]
  },
}
复制代码

在判断如果前一个 ChunkGroup 信息对象的可用模块 resultingAvailableModules 包含后一个 ChunkGroup.chunks[]._modules,则分别建立 import 依赖与对应的 ChunkGroup,前一个 chunkGroup 和后一个 chunkGroup 的关系:

GraphHelpers.connectDependenciesBlockAndChunkGroup(depBlock, depChunkGroup); // ImportDependenciesBlock与chunkGroup建立联系GraphHelpers.connectChunkGroupParentAndChild(chunkGroup, depChunkGroup); // chunkGroup之间建立联系:_children和_parents复制代码

第三阶段

第三阶段主要清理了无用 chunk 并清理相关的联系。

遍历 allCreatedChunkGroups,allCreatedChunkGroups 即为异步被创建的 ChunkGroup,判断 chunkGroup 有没有父的 chunkGroup(_parents),如果没有执行:

for (const chunk of chunkGroup.chunks) {
  const idx = compilation.chunks.indexOf(chunk);
  if (idx >= 0) compilation.chunks.splice(idx, 1);
  chunk.remove('unconnected');
}
chunkGroup.remove('unconnected');
复制代码

即解除 module,chunkGroup,chunk 三者之间的联系。

最终每个 module 与每个 chunk,每个 chunkGroup 和他们之间都建立了联系,优化形成了 chunk Graph。


seal 里继续执行,先将 compilation.modules 按 index 属性大小排序,然后执行:

this.hooks.afterChunks.call(this.chunks);
复制代码

触发插件 WebAssemblyModulesPlugin:设置与 webassembly 相关的报错信息,到此 chunk 生成结束。

module chunk ChunkGroup 区别

module

module 即每一个资源文件的模块对应,如 js / css / 图片 等。由 NormalModule 实例化而来,存于 compilation.modules。

  • module.blocks: 异步模块的依赖

  • module.dependencies 存同步模块的依赖

  • module._chunks 保存 module 所属 chunk 列表

chunk

chunk 即每一个输出文件的对应,包括入口文件,异步加载文件,优化切割后的文件等等,存于 compilation.chunks。

  • chunk._groups: 保存 chunk 所属 ChunkGroup 列表

  • chunk._modules: 由哪些 module 组成

ChunkGroup

ChunkGroup 一般包含一个 chunk(入口 chunk 或异步模块的 chunk)。entrypoint 就是一个 ChunkGroup,里包含入口 chunk。存于 compilation.chunkGroups。

  • ChunkGroup.chunks: 由哪些 chunk 组成

  • ChunkGroup._blocks: 保存异步依赖 ImportDependenciesBlock

  • ChunkGroup._children: 保存子 ChunkGroup

  • ChunkGroup._parent: 保存父 ChunkGroup

本章小结

  1. 在 finish 回调中执行的 seal 方法里,包含了海量钩子用于我们侵入 webpack 的封包阶段;

  1. 在遍历入口文件实例化生成 chunk 时,同时实例化了 Entrypoint 等,并建立了入口 module 和 chunk,Entrypoint 之间的联系;

  1. 通过 buildChunkGraph 的三个阶段,让所有的 module、chunk、chunkGroup 之间都建立了联系,形成了 chunk Graph。

  1. 最后触发钩子 afterChunks 标志这 chunk 生成结束。

作者:Korey

链接:https://juejin.cn/post/6844904047233728526

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值