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

原文首發於 blog.flqin.com。若有錯誤,請聯繫筆者。分析碼字不易,轉載請代表出處,謝謝!webpack

this._addModuleChain 的回調裏,獲得了生成的入口 module。觸發 compilation.hooks:succeedEntry 後,執行 return callback(null, module),回到文件 Compile.jscompilemake 鉤子的回調裏:web

this.hooks.make.callAsync(compilation, err => {
  //...
  compilation.finish(err => {
    //...
    compilation.seal(err => {
      //...
      this.hooks.afterCompile.callAsync(compilation, err => {
        //...
        return callback(null, compilation);
      });
    });
  });
});
複製代碼

compilation.finish

執行 compilation.finish,觸發 compilation.hooksfinishModules,執行插件 FlagDependencyExportsPlugin 註冊的事件,做用是遍歷全部 moduleexport 出來的變量以數組的形式,單獨存儲到 module.buildMeta.providedExports變量下。json

而後執行 reportDependencyErrorsAndWarnings 收集生成每個 module 時暴露出來的 errwarning數組

最後走回調執行 compilation.seal緩存

compilation.seal

compilation.seal 裏觸發了海量 hooks,爲咱們侵入 webpack 構建流程提供了海量鉤子。先執行(咱們先略過沒有註冊方法的鉤子):安全

this.hooks.seal.call();
複製代碼

觸發插件 WarnCaseSensitiveModulesPlugin:模塊文件路徑須要區分大小寫的警告異步

this.hooks.optimizeDependencies.call(this.modules);
複製代碼

production 模式會觸發插件:ide

  • SideEffectsFlagPlugin:識別 package.json 或者 module.rulessideEffects 標誌(純的 ES2015 模塊),安全地刪除未用到的 export 導出
  • FlagDependencyUsagePlugin:編譯時標記依賴 unused harmony export 用於 Tree shaking

chunk 初始化

在觸發 compilation.hooks:beforeChunks 後,開始遍歷入口對象 this._preparedEntrypoints,爲每個入口生成一個 chunk優化

const chunk = this.addChunk(name);
複製代碼

該方法裏作了緩存判斷後執行 new Chunk(name),並同時添加 chunkCompilation.chunks,繼續執行:ui

const entrypoint = new Entrypoint(name);
複製代碼

Entrypoint 類擴展於 ChunkGroup 類,是 chunks 的集合,主要用來優化 chunk graph

繼續執行設置了 Compilation.runtimeChunk & Compilation.namedChunkGroups & Compilation.entrypoints & Compilation.chunkGroupsChunkGroup.origins,而後執行:

GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
GraphHelpers.connectChunkAndModule(chunk, module);
複製代碼

創建了 chunkentrypointchunkmodule 之間的聯繫,而後執行:

this.assignDepth(module);
複製代碼

根據各個模塊依賴的深度(屢次依賴取最小值)設置 module.depth,入口模塊則爲 depth = 0

遍歷完 this._preparedEntrypoints 後,而後執行:

生成 chunk graph

buildChunkGraph(this, /** @type {Entrypoint[]} */ (this.chunkGroups.slice()));
複製代碼

buildChunkGraph 用於生成並優化 chunk 依賴圖,創建起各模塊以前的關係。分爲三階段:

// PART ONE

visitModules(compilation, inputChunkGroups, chunkGroupInfoMap, chunkDependencies, blocksWithNestedBlocks, allCreatedChunkGroups);

// PART TWO

connectChunkGroups(blocksWithNestedBlocks, chunkDependencies, chunkGroupInfoMap);

// Cleaup work

cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
複製代碼

第一階段

先執行:

const blockInfoMap = extraceBlockInfoMap(compilation);
複製代碼

獲得一個 map 結構: module 與該 module 內導入其餘模塊的關係,同步存入 modules,異步存入 blocks。以 demo 爲例,獲得 blockInfoMap

{
  //...map結構
  0:{
    key:NormalModule, //a
    value:{
      blocks:[ImportDependenciesBlock],//異步
      modules:[NormalModule] //b modules爲set結構
    }
  },
  1:{
    key: ImportDependenciesBlock,
    value:{
      blocks: [],
      modules:[NormalModule] //c
    }
  }
  2:{
    key: NormalModule, //c
    value:{
      blocks: [ImportDependenciesBlock],
      modules:[NormalModule] //d
    }
  }
  //........
}
複製代碼

繼續執行,設置了 queue 數組,push 入口 module 和對應的 action 等信息組成的對象,用於 while 循環;設置了 chunkGroupInfoMap,他映射了 chunkGroup 和與他相關的信息對象,而後執行:

while (queue.length) {
  //...
  while (queue.length) {
    //...
    if (chunkGroup !== queueItem.chunkGroup) {
      // 重置更新chunkGroup
    }
    switch (queueItem.action) {
      case ADD_AND_ENTER_MODULE: {
        // 創建chunk和module之間的聯繫
      }
      case ENTER_MODULE: {
        // 設置 chunkGroup._moduleIndices 和 module.index,而後 queue.push 一個新的該 module 的 queueItem,action 設爲 LEAVE_MODULE
      }
      case PROCESS_BLOCK: {
        // 1. 遍歷 blockInfoMap 裏的同步模塊 modules,若是對應 chunk 已有此模塊則跳過,若是 minAvailableModules 有此模塊則一個新的 queueItem 存入 skippedItems 數組,沒有則該 queueItem 存入 queue,其中 queue 的 action 都設爲 ADD_AND_ENTER_MODULE
        // 2. 遍歷 blockInfoMap 裏的異步模塊 blocks
        // 2.1 建立一個對應import依賴的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
      }
      case LEAVE_MODULE: {
        // 設置 chunkGroup._moduleIndices2 和 module.inde2
      }
    }
  }
  while (queueConnect.size > 0) {
    // 1. 在 chunkGroupInfoMap 中設置前一個 ChunkGroup 的信息對象的 resultingAvailableModules, children
    // 2. 在 chunkGroupInfoMap 中初始化新的 ChunkGroup 與他相關的信息對象的映射並設置了 availableModulesToBeMerged
    if (outdatedChunkGroupInfo.size > 0) {
      // 1.獲取設置新的 ChunkGroup 信息對象的 minAvailableModules
      // 2.若是新的 ChunkGroup 信息對象的 skippedItems 不爲空則 push 到 queue
      // 3.若是新的 ChunkGroup 信息對象的 children 不爲空,則更新 queueConnect 遞歸循環
    }
  }
  // 把queueDelayed 放入queue走while的最外層循環,目的的同步循環處理完後,而後才處理異步module
  if (queue.length === 0) {
    const tempQueue = queue;
    queue = queueDelayed.reverse();
    queueDelayed = tempQueue;
  }
}
複製代碼
  • 在內部 whilequeue.length 循環裏( while+push 防遞歸爆棧,後序深度優先),從入口 module 開始,解析了全部同步 module 並創建了 modulechunk 的聯繫;解析了全部第一層異步的 module,併爲每一個不一樣 mudule 都新建了 chunkGroupchunk 並創建了二者的聯繫。
  • 而後在 whilequeueConnect.size 的循環裏,更新了 chunkGroupInfoMap 中前一個 ChunkGroup 的信息對象和初始化了新的 ChunkGroup 的信息對象,並獲取了最小可用模塊。
  • 同步模塊循環處理結束後,開始處理異步 module,將 queueDelayed 賦給 queue,走外部 whilequeue.length 的循環。
  • 處理異步模塊的時候,queue 裏的 blockImportDependenciesBlock 依賴,而後更新 chunkGroup 後, switchPROCESS_BLOCK 得到本次異步對應的真正模塊,後面的處理數據都將在新的 ChunkGroup 信息對象上。就這樣循環處理,最終獲得一個 Map 結構的 chunkGroupInfoMap。以本 demo 爲例,獲得:
{
  //...map結構
  0:{
    key:Entrypoint, //groupDebugId:5000
    value:{
      availableModulesToBeMerged:Array(0)
      children:Set(1) {} //ChunkGroup 5001
      chunkGroup:Entrypoint
      minAvailableModules:Set(0)
      minAvailableModulesOwned:true
      resultingAvailableModules:Set(3)
      skippedItems:Array(0)
    }
  },
  1:{
    key: ChunkGroup, //groupDebugId:5001
    value:{
      availableModulesToBeMerged:Array(0)
      children:Set(1) {} //ChunkGroup 5002
      chunkGroup:Entrypoint
      minAvailableModules:Set(3)
      minAvailableModulesOwned:true
      resultingAvailableModules:Set(5)
      skippedItems:Array(0)
    }
  }
  2:{
    key: ChunkGroup, //groupDebugId:5002
    value:{
      availableModulesToBeMerged:Array(0)
      children:undefined
      chunkGroup:Entrypoint
      minAvailableModules:Set(5)
      minAvailableModulesOwned:true
      resultingAvailableModules:undefined
      skippedItems:Array(1)
    }
  }
}
複製代碼

第二階段

遍歷 chunkDependencieschunkDependenciesMap 結構,保存着前一個 ChunkGroup 與新的 ChunkGroupimport 依賴之間的映射:

{
  //...map結構
  0:{
    key:Entrypoint, //groupDebugId:5000
    value:[
      {
        block:ImportDependenciesBlock,
        chunkGroup:ChunkGroup //groupDebugId:5001
      }
    ]
  },
  1:{
    key:ChunkGroup, //groupDebugId:5001
    value:[
      {
        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
複製代碼

第三階段

遍歷 allCreatedChunkGroupsallCreatedChunkGroups 即爲異步被建立的 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.modulesindex 屬性大小排序,而後執行:

this.hooks.afterChunks.call(this.chunks);
複製代碼

觸發插件 WebAssemblyModulesPlugin:設置與 webassembly 相關的報錯信息,到此 chunk 生成結束。

本章小結

  1. finish 回調中執行的 seal 方法裏,包含了海量鉤子用於咱們侵入 webpack 的封包階段;
  2. 在遍歷入口文件實例化生成 chunk 時,同時實例化了 Entrypoint 等,並創建了入口 modulechunkEntrypoint 之間的聯繫;
  3. 經過 buildChunkGraph 的三個階段,讓全部的 module、chunk、chunkGroup 之間都創建了聯繫,造成了 chunk Graph
  4. 最後觸發鉤子 afterChunks 標誌這 chunk 生成結束。
相關文章
相關標籤/搜索