淺析 webpack 打包流程(原理) 三 - 生成 chunk

接上文:淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module

五裸扶、生成 chunk

生成 chunk 階段概述:在compilation.finish回調(diào)中執(zhí)行的 seal 方法中详民,觸發(fā)海量鉤子,就此侵入 webpack 的封包階段;
1.首先對(duì)所有 importexport 做標(biāo)記俭嘁,以實(shí)現(xiàn)最后構(gòu)建資源階段的 treeshaking宰啦;
2.遍歷入口文件為每個(gè)入口生成初始 chunk 的同時(shí),也實(shí)例化了 EntryPoint(繼承自 ChunkGroup 類(lèi))奸例,并建立了入口 module 和 chunk彬犯、entryPoint 之間的聯(lián)系
3.通過(guò) buildChunkGraph 的三個(gè)階段查吊,生成異步 chunk 和 包含它的chunkGroup谐区,將所有 module、chunk逻卖、chunkGroup 都建立起關(guān)聯(lián)宋列,形成了 chunkGraph
4.最后將compilation.modules排序评也,再觸發(fā)afterChunks 鉤子虚茶,chunk 生成結(jié)束。
這部分都是 webpack 的預(yù)處理 和 chunks 默認(rèn)規(guī)則的實(shí)現(xiàn)仇参,后面 chunk 優(yōu)化階段會(huì)暴露很多鉤子嘹叫,webpack 會(huì)根據(jù)我們配置的插件來(lái)進(jìn)行優(yōu)化。

上一步我們 addEntry 方法 this._addModuleChain 的傳的回調(diào)里return callback(null, module);诈乒,回到compile方法的 compiler.hooks.make.callAsync()罩扇,執(zhí)行它的回調(diào):

// /lib/Compiler.js
this.hooks.make.callAsync(compilation, err => {
  if (err) return callback(err);
  compilation.finish(err => {
    if (err) return callback(err);
    compilation.seal(err => {
      if (err) return callback(err);
      this.hooks.afterCompile.callAsync(compilation, err => {
        if (err) return callback(err);
        return callback(null, compilation);
      });
    });
  });
});

此時(shí)compilation.modules已經(jīng)有了所有的模塊:a、c、b喂饥、d消约。
執(zhí)行compilation.finish方法,觸發(fā)compilation.hooks:finishModules员帮,執(zhí)行插件 FlagDependencyExportsPlugin 注冊(cè)的事件或粮,作用是遍歷所有 module,將 export 出來(lái)的變量以數(shù)組的形式捞高,單獨(dú)存儲(chǔ)到 module.buildMeta.providedExports 變量下氯材。
然后通過(guò)遍歷為每一個(gè) module 執(zhí)行compilation.reportDependencyErrorsAndWarnings,收集生成它們時(shí)暴露出來(lái)的 err 和 warning硝岗。

最后走回調(diào)執(zhí)行compilation.seal氢哮,提供了海量讓我們侵入 webpack 構(gòu)建流程的 hooks。seal 字面意思是封包型檀,也就是開(kāi)始對(duì)上一步生成的 module 結(jié)果進(jìn)行封裝冗尤。
先執(zhí)行 (我們先略過(guò)沒(méi)有注冊(cè)方法的鉤子)this.hooks.seal.call();,觸發(fā)插件 WarnCaseSensitiveModulesPlugin:在 compilation.warnings 添加 模塊文件路徑需要區(qū)分大小寫(xiě)的警告胀溺。

再是this.hooks.optimizeDependencies.call(this.modules)裂七,production 模式會(huì)觸發(fā)插件:

  • SideEffectsFlagPlugin:識(shí)別 package.json 或者 module.rules 的 sideEffects 標(biāo)記的純 ES2015 模塊(純函數(shù)),安全地刪除未用到的 export 導(dǎo)出仓坞;
  • FlagDependencyUsagePlugin:編譯時(shí)標(biāo)記依賴(lài) unused harmony export 背零,用于 Tree shaking

5.1 chunk 初始化

在觸發(fā)compilation.hooks:beforeChunks后,開(kāi)始遍歷入口對(duì)象 this._preparedEntrypoints扯躺,每個(gè)入口 module 都會(huì)通過(guò)addChunk去創(chuàng)建一個(gè)空 chunk(并添加到compilation.chunks),此時(shí)不包含任何與之相關(guān)聯(lián)的 module蝎困。之后實(shí)例化一個(gè) EntryPoint录语,把它添加到compilation.chunkGroups中。接下來(lái)調(diào)用 GraphHelpers 模塊提供的方法來(lái)建立起 chunkGroup 和 chunk 之間的聯(lián)系禾乘,以及 chunk 和 入口 module 之間的聯(lián)系(這里還未涉及到入口依賴(lài)的 module):

// /lib/Compilation.js
for (const preparedEntrypoint of this._preparedEntrypoints) {
  const module = preparedEntrypoint.module;
  const name = preparedEntrypoint.name;
  // addChunk 方法進(jìn)行緩存判斷后執(zhí)行 new Chunk(name)澎埠,并同時(shí)添加 chunk 到 compilation.chunks
  const chunk = this.addChunk(name);
  // Entrypoint 類(lèi)擴(kuò)展于 ChunkGroup 類(lèi),是 chunks 的集合始藕,主要用來(lái)優(yōu)化 chunk graph
  const entrypoint = new Entrypoint(name); // 每一個(gè) entryPoint 就是一個(gè) chunkGroup
  entrypoint.setRuntimeChunk(chunk); // 設(shè)置 runtimeChunk蒲稳,就是運(yùn)行時(shí) chunk
  entrypoint.addOrigin(null, name, preparedEntrypoint.request);
  this.namedChunkGroups.set(name, entrypoint);
  this.entrypoints.set(name, entrypoint);
  this.chunkGroups.push(entrypoint); // 把 entryPoint 添加到 chunkGroups

  // 建立 chunkGroup 和 chunk 之間的聯(lián)系:
  GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk);
  // 建立 chunk 和 入口 module 之間的聯(lián)系(這里還未涉及到入口的依賴(lài)模塊)
  GraphHelpers.connectChunkAndModule(chunk, module);

  chunk.entryModule = module;
  chunk.name = name;
  // 根據(jù)各個(gè)模塊依賴(lài)的深度(多次依賴(lài)取最小值)設(shè)置 module.depth,入口模塊則為 depth = 0伍派。
  this.assignDepth(module);
}

比如我們的 demo江耀,只配置了一個(gè)入口,那么這時(shí)會(huì)生成一個(gè) chunkGroup(Entrypoint) 和一個(gè) chunk诉植,這個(gè) chunk 目前只包含入口 module祥国。

5.2 生成 chunk graph

執(zhí)行 buildChunkGraph(this, /** @type {Entrypoint[]} */ (this.chunkGroups.slice()));
buildChunkGraph 方法用于生成并優(yōu)化 chunk 依賴(lài)圖,建立起 module、chunk舌稀、chunkGroup 之間的關(guān)系啊犬。分為三階段:

// /lib/buildChunkGraph.js

// PART ONE
visitModules(compilation, inputChunkGroups, chunkGroupInfoMap, chunkDependencies, blocksWithNestedBlocks, allCreatedChunkGroups);

// PART TWO
connectChunkGroups(blocksWithNestedBlocks, chunkDependencies, chunkGroupInfoMap);

// Cleaup work
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
第一階段 visitModules

先執(zhí)行:visitModules 的 const blockInfoMap = extraceBlockInfoMap(compilation);
對(duì)本次 compliation.modules 進(jìn)行一次迭代遍歷,意在完完整整收集所有的模塊(同步壁查、異步)及每個(gè)模塊的直接依賴(lài)觉至。

具體處理邏輯:
遍歷每個(gè)模塊compilation.modules,先把其同步依賴(lài)(dependencies)存入 modules Set 集睡腿,再遍歷異步依賴(lài)(blocks)语御,把每個(gè)異步依賴(lài)存入模塊的 blocks 數(shù)組。
然后這些異步依賴(lài)會(huì)再加入到while循環(huán)遍歷中(作為一個(gè)模塊)嫉到,不僅為它在blockInfoMap單獨(dú)建立起一個(gè)ImportDependenciesBlock類(lèi)型的數(shù)據(jù)(里面包含這個(gè)異步 module 本身)沃暗,再去遍歷它存儲(chǔ)一個(gè)NormalModule類(lèi)型的數(shù)據(jù)(包含它的同步 modules 和異步 blocks),之后遇到異步依賴(lài)都是優(yōu)先這樣處理異步依賴(lài)何恶。

遍歷結(jié)束??后會(huì)建立起基本的 Module Graph孽锥,包括所有的NormalModuleImportDependenciesBlock,存儲(chǔ)在一個(gè)blockInfoMap Map 表當(dāng)中(每一項(xiàng)的值都是它們的直接依賴(lài)细层,同步存 modules惜辑,異步存 blocks)。
【淺析 webpack 打包流程(原理) - 案例 demo】為例疫赎,得到 blockInfoMap:

Map結(jié)構(gòu)盛撑,一共6項(xiàng),未截完全

看具體數(shù)據(jù)應(yīng)該能大致理解碰到異步就去迭代遍歷異步的處理順序:

// blockInfoMap
{
  0: {
    key: NormalModule,  // a捧搞,debugId:1000抵卫,depth:0
    value: {
      blocks: [ImportDependenciesBlock], // 異步 c
      modules: [NormalModule] // b (modules為set結(jié)構(gòu)) debugId:1002,depth:1
    }
  },
  1: {
    key: ImportDependenciesBlock,
    value: {
      blocks: [],
      modules: [NormalModule] // c胎撇,debugId:1001介粘,depth:1
    }
  },
  2: {
    key: NormalModule, // c,debugId:1001晚树,depth:1
    value: {
      blocks: [ImportDependenciesBlock], // 異步 b
      modules: [NormalModule] // d姻采,debugId:1004,depth:2
    }
  }
  3: {
    key: ImportDependenciesBlock,
    value: {
      blocks: [],
      modules: [NormalModule] // b爵憎,debugId:1002慨亲,depth:1
    }
  },
  4: {
    key: NormalModule, // b,debugId:1002宝鼓,depth:1
    value: {
      blocks: [],
      modules: [NormalModule] // d刑棵,debugId:1004,depth:2
    }
  },
  5: {
    key: NormalModule, // d愚铡,debugId:1004铐望,depth:2
    value: {
      blocks: [],
      modules: []
    }
  }
}

存儲(chǔ)完入口模塊 a 的直接依賴(lài)(同步和異步),會(huì)優(yōu)先先去循環(huán)處理它的異步依賴(lài) c,收集 c 的直接依賴(lài)(同步和異步)正蛙,然后又優(yōu)先遍歷 c 的異步依賴(lài)...過(guò)程中遇到的所有異步依賴(lài)都會(huì)建立一個(gè)ImportDependenciesBlock對(duì)象督弓,值內(nèi)包含一項(xiàng)內(nèi)容為它自身的NormalModule。同時(shí)假如有重復(fù)的異步模塊乒验,會(huì)生成多項(xiàng)ImportDependenciesBlock愚隧。其余會(huì)生成幾項(xiàng)和 compliation.modules 一一對(duì)應(yīng)的NormalModule(a、b锻全、c狂塘、d)

接著用reduceChunkGroupToQueueItem函數(shù)處理目前只有一個(gè) EntryPoint 的 chunkGroups:

// 用 reduceChunkGroupToQueueItem 處理每一個(gè) chunkGroup
let queue = inputChunkGroups
  .reduce(reduceChunkGroupToQueueItem, [])
  .reverse();

將它轉(zhuǎn)化為一個(gè) queue 數(shù)組,每項(xiàng)為入口 module鳄厌、chunk 以及對(duì)應(yīng)的 action 等信息組成的對(duì)象荞胡,詳見(jiàn)下面源碼
說(shuō)明下action:模塊需要被處理的階段類(lèi)型了嚎,不同類(lèi)型的模塊會(huì)經(jīng)過(guò)不同的流程處理泪漂,初始為 ENTER_MODULE: 1,全部類(lèi)型如下:

  • ADD_AND_ENTER_MODULE = 0
  • ENTER_MODULE = 1
  • PROCESS_BLOCK = 2
  • LEAVE_MODULE = 3

緊跟著設(shè)置chunkGroupInfoMap歪泳,它映射了每個(gè) chunkGroup 和與它相關(guān)的信息對(duì)象萝勤。

// /lib/buildChunkGraph.js
for (const chunk of chunkGroup.chunks) {
  const module = chunk.entryModule;
  queue.push({
    action: ENTER_MODULE, // 需要被處理的模塊類(lèi)型,不同處理類(lèi)型的模塊會(huì)經(jīng)過(guò)不同的流程處理呐伞,初始為 ENTER_MODULE: 1
    block: module, // 入口 module
    module, // 入口 module
    chunk, // seal 階段一開(kāi)始為每個(gè)入口 module 創(chuàng)建的 chunk敌卓,只包含入口 module
    chunkGroup // entryPoint
  });
}
chunkGroupInfoMap.set(chunkGroup, {
  chunkGroup,
  minAvailableModules: new Set(), // chunkGroup 可追蹤的最小 module 數(shù)據(jù)集
  minAvailableModulesOwned: true,
  availableModulesToBeMerged: [], // 遍歷環(huán)節(jié)所使用的 module 集合
  skippedItems: [],
  resultingAvailableModules: undefined,
  children: undefined
 });

然后基于module graph,對(duì) queue 進(jìn)行了 2 層遍歷伶氢。我們提供的 demo 是單入口趟径,因此 queue 只有一項(xiàng)數(shù)據(jù)。

// /lib/buildChunkGraph.js
// 基于 Module graph 的迭代遍歷癣防,不用遞歸寫(xiě)是為了防止可能的堆棧溢出
while (queue.length) { // 外層遍歷
  logger.time("visiting");
  while (queue.length) { // 內(nèi)層遍歷
    const queueItem = queue.pop(); // 刪除并返回 queue 數(shù)組的最后一項(xiàng)
    // ...
    if (chunkGroup !== queueItem.chunkGroup) {
      // 重置更新 chunkGroup
    }
    switch (queueItem.action) {
      case ADD_AND_ENTER_MODULE: {
        // 如果 queueItem.module 在 minAvailableModules蜗巧,則將該 queueItem 存入 skippedItems
        if (minAvailableModules.has(module)) {
          Items.push(queueItem);
          break;
        }
        // 建立 chunk 和 module 之間的聯(lián)系,將依賴(lài)的 module 存入該 chunk 的 _modules 屬性里劣砍,將 chunk 存入 module 的 _chunks 里
        // 如果 module 已經(jīng)在 chunk 中則結(jié)束 switch
        if (chunk.addModule(module)) {
          module.addChunk(chunk);
        }
      }
      case ENTER_MODULE: {
        // 設(shè)置 chunkGroup._moduleIndices 和 module.index惧蛹,然后
        // ...  
        // 給 queue push 一項(xiàng) queueItem(action 為 LEAVE_MODULE)扇救,供后面遍歷的流程中使用刑枝。
        queue.push({
          action: LEAVE_MODULE,
          block,
          module,
          chunk,
          chunkGroup
        });
      }
      case PROCESS_BLOCK: {
        // 1. 從 blockInfoMap 中查詢(xún)到當(dāng)前 queueItem 的模塊數(shù)據(jù)
        const blockInfo = blockInfoMap.get(block);

        // 2. 遍歷當(dāng)前模塊的同步依賴(lài) 沒(méi)有則存入 queue,其中 queueItem.action 都設(shè)為 ADD_AND_ENTER_MODULE
        for (const refModule of blockInfo.modules) {
          if (chunk.containsModule(refModule)) {
            // 跳過(guò)已經(jīng)存在于 chunk 的同步依賴(lài)
            continue;
          }
          // 如果已經(jīng)存在于父 chunk (chunkGroup 可追蹤的最小 module 數(shù)據(jù)集 -- minAvailableModules)
          // 則將該 queueItem push 到 skipBuffer(action 為 ADD_AND_ENTER_MODULE)迅腔,并跳過(guò)該依賴(lài)的遍歷

          // 倒序?qū)?skipBuffer 添加 skippedItems装畅,queueBuffer 添加到 queue

          // enqueue the add and enter to enter in the correct order
          // this is relevant with circular dependencies
          // 以上都不符合則將 queueItem push 到 queueBuffer(action 為 ADD_AND_ENTER_MODULE)
          queueBuffer.push({
            action: ADD_AND_ENTER_MODULE,
            block: refModule,
            module: refModule,
            chunk,
            chunkGroup
          });
        }
        
        // 3. 用 iteratorBlock 方法迭代遍歷模塊所有異步依賴(lài) blocks
        for (const block of blockInfo.blocks) iteratorBlock(block);

        if (blockInfo.blocks.length > 0 && module !== block) {
          blocksWithNestedBlocks.add(block);
        }
      }
      case LEAVE_MODULE: {
        // 設(shè)置 chunkGroup._moduleIndices2 和 module.index2
      }
    }
  }
  // 上文 while (queue.length) 從入口 module 開(kāi)始,循環(huán)將所有同步依賴(lài)都加入到同一個(gè) chunk 里沧烈,將入口 module 及它的同步依賴(lài)?yán)锏漠惒揭蕾?lài)都各自新建了chunkGroup 和 chunk掠兄,并將異步模塊存入 queueDelayed,異步依賴(lài)中的異步依賴(lài)還未處理。

  while (queueConnect.size > 0) {
    // 計(jì)算可用模塊
    // 1. 在 chunkGroupInfoMap 中設(shè)置前一個(gè) chunkGroup 的 info 對(duì)象的 resultingAvailableModules蚂夕、children
    // 2. 在 chunkGroupInfoMap 中初始化新的 chunkGroup 與他相關(guān)的 info 對(duì)象的映射并設(shè)置了 availableModulesToBeMerged
    if (outdatedChunkGroupInfo.size > 0) {
      // 合并可用模塊
      // 1. 獲取/設(shè)置新的 chunkGroup info 對(duì)象的 minAvailableModules
      // 2. 將新的 chunkGroup info 對(duì)象的 skippedItems push 到 queue
      // 3. 如果新的 chunkGroup info 對(duì)象的 children 不為空迅诬,則更新 queueConnect 遞歸循環(huán)
    }
  }
  // 當(dāng) queue 隊(duì)列的所有項(xiàng)都被處理后,執(zhí)行 queueDelayed
  // 把 queueDelayed 放入 queue 走 while 的外層循環(huán)婿牍,目的是在所有同步依賴(lài) while 處理完之后侈贷,才處理異步模塊
  // 如果異步模塊里還有異步依賴(lài),將放到一下次的 queueDelayed 走 while 的外層循環(huán)
  if (queue.length === 0) {
    const tempQueue = queue; // ImportDependenciesBlock
    queue = queueDelayed.reverse();
    queueDelayed = tempQueue;
  }
}

while 循環(huán)只要條件為 true 就會(huì)一直循環(huán)代碼塊等脂,只有當(dāng)條件不成立或者內(nèi)部有if(condition){ return x;}俏蛮、if(condition){ break; }才能跳出循環(huán)。( while+push 防遞歸爆棧上遥,后序深度優(yōu)先)

進(jìn)入內(nèi)層遍歷搏屑,匹配到case ENTER_MODULE,會(huì)給 queue push 一個(gè) action 為LEAVE_MODULE的 queueItem 項(xiàng)供后面遍歷流程中使用粉楚。然后進(jìn)入到PROCESS_BLOCK階段:

blockInfoMap中查詢(xún)到當(dāng)前 queueItem 的模塊數(shù)據(jù)辣恋,只有當(dāng)前模塊的直接依賴(lài),在本例就是:

blockInfo

接下來(lái)遍歷模塊的所有單層同步依賴(lài) modules解幼,跳過(guò)已經(jīng)存在于 chunk 的同步依賴(lài)抑党;如果同步依賴(lài)已在 minAvailableModules(chunkGroup 可追蹤的最小 module 數(shù)據(jù)集),則將 queueItem push 到 skipBuffer撵摆,然后跳出該依賴(lài)的遍歷底靠;以上都沒(méi)有則將 queueItem 存入緩沖區(qū) queueBuffer,action 都設(shè)為 ADD_AND_ENTER_MODULE(即下次遍歷這個(gè) queueItem 時(shí)特铝,會(huì)先進(jìn)入到 ADD_AND_ENTER_MODULE)暑中。同步 modules 遍歷完,將得到的 queueBuffer 反序添加到 queue鲫剿。也就是后面的內(nèi)層遍歷中鳄逾,會(huì)優(yōu)先處理同步依賴(lài)嵌套的同步模塊,(不重復(fù)地)添加完再去處理同級(jí)同步依賴(lài)灵莲。

接下來(lái)調(diào)用iteratorBlock來(lái)迭代遍歷當(dāng)前模塊的單層異步依賴(lài) blocks雕凹,方法內(nèi)部主要實(shí)現(xiàn)的是:

  1. 調(diào)用addChunkInGroup為這個(gè)異步 block 創(chuàng)建一個(gè) chunk 和 chunkGroup,同時(shí)建立這兩者之間的聯(lián)系政冻。此時(shí)這個(gè) chunk 是空的,還沒(méi)有添加任何它的依賴(lài)明场;
  2. 把 chunkGroup 添加到compilation.chunkGroups(Array) 和compilation.namedChunkGroups(Map),chunkGroupCounters(計(jì)數(shù) Map)苦锨、blockChunkGroups(映射依賴(lài)和 ChunkGroup 關(guān)系的 Map)趴泌、allCreatedChunkGroups(收集被創(chuàng)建的 ChunkGroup Set)拉庶。
  3. 把這項(xiàng) block 和 block 所屬的 chunkGroup 以對(duì)象的形式 push 到 chunkDependencies Map 表中 ?? 當(dāng)前 module 所屬 chunkGroup (Map 的 key)下,每一都是{ block: ImportDependenciesBlock, chunkGroup: chunkGroup }的形式痹筛。建立起 block 和它所屬 chunkGroup 和 父 chunkGroup 之間的依賴(lài)關(guān)系廓鞠。chunkDependencies 表主要用于后面優(yōu)化 chunk graph;
  4. 更新 queueConnect床佳,建立父 chunkGroup 與新 chunkGroup 的映射;
  5. 向 queueDelayed 中 push 一個(gè) { action:PROCESS_BLOCK, module: 當(dāng)前 block 所屬 module, block: 當(dāng)前異步 block, chunk: 新 chunkGroup 中的第一個(gè) chunk, chunkGroup: 新 chunkGroup } 砌们,該項(xiàng)主要用于 queue 的外層遍歷杆麸。

iteratorBlock處理完當(dāng)前模塊所有直接異步依賴(lài) (block) 后,結(jié)束本輪內(nèi)層遍歷浪感。
前面為 queue push 了兩項(xiàng) queueItem昔头,一個(gè)是入口模塊 a(action 為 LEAVE_MODULE),一個(gè)是同步模塊 b(action 為 ADD_AND_ENTER_MODULE)影兽。因此繼續(xù)遍歷 queue 數(shù)組揭斧,反序先遍歷 b,匹配到ADD_AND_ENTER_MODULE峻堰,把 b 添加到 入口 chunk (_modules屬性)中讹开,也把入口 chunk 存入 b 模塊的_chunks屬性里。然后進(jìn)入ENTRY_MODULE階段捐名,標(biāo)記為LEAVE_MODULE旦万,添加到 queue。
然后進(jìn)入PROCESS_BLOCK處理 b 的同步依賴(lài)和異步依賴(lài)(過(guò)程如上文):

??盡力說(shuō)得通俗些的總結(jié):
將模塊直接同步依賴(lài)標(biāo)記為ADD_AND_ENTER_MODULE添加到 queue 用于接下來(lái)的遍歷镶蹋,push 時(shí)其余屬性 block 和 module 是它本身成艘, chunk、chunkGroup 不變贺归;
直接異步依賴(lài)則標(biāo)記為PROCESS_BLOCK添加到用于外層遍歷的 queueDelayed淆两,push 時(shí)傳的是新的 chunk 和 chunkGroup,block 是它本身牧氮,module 是它的父模塊琼腔。同時(shí)會(huì)為此異步依賴(lài)新建一個(gè)包含一個(gè)空 chunk 的 chunkGroup瑰枫。
外層 while 的執(zhí)行時(shí)機(jī)是等所有入口模塊的同步依賴(lài)(包括間接)都處理完后踱葛。
建立初步的 chunk graph 順序可以簡(jiǎn)單地捋成:
1.首先入口和所有(直接/間接)同步依賴(lài)形成一個(gè) chunkGroup 組(添加模塊的順序?yàn)椋合仁峭揭蕾?lài)嵌套的同步依賴(lài)都處理完丹莲,再去遍歷平級(jí)的同步依賴(lài));
2.然后按每個(gè)異步依賴(lài)的父模塊被處理的順序甥材,為它們各自建立一個(gè) chunk 和 chunkGroup洲赵。異步 chunk 中只會(huì)包含入口 chunk 中不存在的同步依賴(lài)叠萍。相同的異步模塊會(huì)重復(fù)創(chuàng)建 chunk。

然后走while (queueConnect.size > 0)循環(huán)格郁,更新了chunkGroupInfoMap中父 chunkGroup 的 info 對(duì)象例书,初始化新的 chunkGroup info 對(duì)象决采,并獲取了最小可用模塊树瞭。

然后等內(nèi)層循環(huán)把 queue 數(shù)組 (內(nèi)層只管模塊所有同步依賴(lài)) 一個(gè)個(gè)反序處理完(數(shù)量為0)旺嬉,就把 queueDelayed 賦給 queue 厨埋,走外部while(queue.length)循環(huán)處理異步依賴(lài) (真正處理異步模塊)雨效。這時(shí)這些 queueItem 的 action 都為PROCESS_BLOCK徽龟,block 都為 ImportDependenciesBlock 依賴(lài)据悔。更新 chunkGroup 后, switch 直接走 PROCESS_BLOCK 獲得異步項(xiàng)對(duì)應(yīng)的真正模塊朱盐,和之前同步模塊一樣處理(有異步依賴(lài)就新建 chunk 和 chunkGroup [無(wú)論之前無(wú)為同樣的異步塊創(chuàng)建過(guò) chunkGroup,均會(huì)重復(fù)創(chuàng)建]骇径,并放入 queueDelayed)破衔,處理數(shù)據(jù)都將存儲(chǔ)在新的 chunkGroup 對(duì)象上运敢。最終得到一個(gè) Map 結(jié)構(gòu)的chunkGroupInfoMap迄沫。以 demo 為例:

children 為每項(xiàng)的子 chunkGroup羊瘩,resultingAvailableModules 為本 chunkGroup 可用的模塊

// chunkGroupInfoMap Map 對(duì)象
[
  0: {
    key: Entrypoint, // groupDebugId: 5000
    value: {
      availableModulesToBeMerged: Array(0) // 遍歷環(huán)節(jié)所使用的 module 集合
      children: Set(1) {} // 子 chunkGroup尘吗,groupDebugId: 5001
      chunkGroup: Entrypoint
      minAvailableModules: Set(0) // chunkGroup 可追蹤的最小 module 數(shù)據(jù)集
      minAvailableModulesOwned: true
      resultingAvailableModules: Set(3) // 這個(gè) chunkGroup 的可用模塊 a b d
      skippedItems: Array(0)
    }
  },
  1: {
    key: ChunkGroup, // groupDebugId: 5001
    value: {
      availableModulesToBeMerged: Array(0)
      children: Set(1) {} // 子 chunkGroup睬捶,groupDebugId: 5002
      chunkGroup: Entrypoint
      minAvailableModules: Set(3) // a b d
      minAvailableModulesOwned: true
      resultingAvailableModules: Set(4) // 這個(gè) chunkGroup 的可用模塊 a b d c
      skippedItems: Array(1) // d
    }
  }
  2: {
    key: ChunkGroup, // groupDebugId: 5002
    value: {
      availableModulesToBeMerged: Array(0)
      children: undefined
      chunkGroup: Entrypoint
      minAvailableModules: Set(4)  // a b d c
      minAvailableModulesOwned: true
      resultingAvailableModules: undefined
      skippedItems: Array(1) // b
    }
  }
]

此時(shí)的compilation.chunkGroups有三個(gè) chunkGroup:
包含一個(gè)_modules: { a, b, d } chunk 的 EntryPoint;包含一個(gè)_modules: { c } chunk 的 chunkGroup(入口異步引入的 c 創(chuàng)建)觉渴;包含一個(gè)空 chunk 的 chunkGroup(c 引入 b 時(shí)創(chuàng)建)案淋。
即入口和它所有同步依賴(lài)組成一個(gè) chunk(包含在 EntryPoint 內(nèi))誉碴,每個(gè)異步依賴(lài)成為一個(gè) chunk(各自在一個(gè) chunkGroup 內(nèi))。遇到相同的異步模塊會(huì)重復(fù)創(chuàng)建 chunk 和 chunkGroup,處理 chunk 同步模塊時(shí)遇到已存在于入口 chunk 的模塊將跳過(guò)蹬屹,不再存入chunk._modules慨默。

初步的 chunk graph
第二階段 connectChunkGroups

遍歷 chunkDependencies厦取,根據(jù) ImportDependenciesBlock(block) 建立了不同 chunkGroup 之間的父子關(guān)系虾攻。
chunkDependencies 只保存有子 chunkGroup 的 chunkGroup(也就是 EntryPoint 和霎箍,有異步依賴(lài)的異步模塊創(chuàng)建的 chunkGroup 才會(huì)被存到里面) 漂坏,屬性是 chunkGroup, 值是 chunkGroup 的所有 子 chunkGroup 和 異步依賴(lài)組成的對(duì)象 的數(shù)組:

// chunkDependencies Map 對(duì)象
[
  0: {
    key: Entrypoint, // groupDebugId: 5000
    value: [
      { block: ImportDependenciesBlock, chunkGroup: ChunkGroup }, // groupDebugId: 5001
      // { block: ImportDependenciesBlock, chunkGroup: ChunkGroup }, // groupDebugId: 5003
      // 實(shí)際項(xiàng)目一般會(huì)存在多項(xiàng)
    ]
  },
  1: {
    key: ChunkGroup, // groupDebugId: 5001
    value: [
      { block: ImportDependenciesBlock, chunkGroup: ChunkGroup } // groupDebugId: 5002
    ]
  },
]

文字很繞驯绎,關(guān)于 chunkDependencies 用一個(gè)模塊更多的圖就容易理解得多了:

多模塊 demo2

這個(gè)例子的 chunkDependencies 是這樣的:

// 簡(jiǎn)單地用  groupDebugId 指代子 chunkgroup 和 子 chunkgroup 的 chunk
{
  { key: EntryPoint 5000, value: [5001, 5002, 5003, 5004] },
  { key: ChunkGroup 5001, value: [5005, 5006] },
  { key: ChunkGroup 5002, value: [5007] }
}

遍歷時(shí)子 chunkgroup 的chunks[]._modules如果有父 chunkGroup 的可用模塊resultingAvailableModules中不包含的新模塊,則分別建立異步依賴(lài)與對(duì)應(yīng) chunkGroup(互相添加到彼此的chunkGroup_blocks)赴叹、父 chunkGroup 和子 chunkGroup 的父子關(guān)系(互相添加到彼此的_children_parents):
(resultingAvailableModules通過(guò)查詢(xún)chunkGroupInfoMap.get(父chunkGroup)獲取)

如上面 demo2指蚜,ChunkGroup 5001 的可用模塊是a b d e c j绽媒,它的子 ChunkGroup 5005 是由 b 創(chuàng)建的(且因?yàn)椴粫?huì)重復(fù)創(chuàng)建入口 chunk 中存在的同步模塊是辕, 5005 的 chunk 并不包含任何模塊)获三,沒(méi)有新模塊疙教,故而沒(méi)有建立起關(guān)系贞谓。而子ChunkGroup 5006 有新模塊 k裸弦,就建立起了上述關(guān)系理疙。

// /lib/buildChunkGraph.js
// ImportDependenciesBlock 與 chunkGroup 建立聯(lián)系,互相添加到彼此的 chunkGroup 和 _blocks
GraphHelpers.connectDependenciesBlockAndChunkGroup(
  depBlock,
  depChunkGroup
); 

// chunkGroup 之間建立聯(lián)系:互相添加到彼此的 _children 和 _parents
GraphHelpers.connectChunkGroupParentAndChild(
  chunkGroup,
  depChunkGroup
);
第三階段 cleanupUnconnectedGroups

清理無(wú)用 chunk 并清理相關(guān)的聯(lián)系。
通過(guò)遍歷allCreatedChunkGroups择吊,如果遇到在第二階段沒(méi)有建立起聯(lián)系的 chunkGroup(如上面 demo2 chunkGroup 5005)槽奕,那么就將這些 chunkGroup 中的所有 chunk 從 chunk graph 依賴(lài)圖當(dāng)中剔除掉 ( demo2 中的異步 b chunk 此時(shí)被刪除 )所森。
allCreatedChunkGroups即異步模塊被創(chuàng)建的 chunkGroup焕济,依次判斷 chunkGroup 有無(wú)父 chunkGroup(_parents)晴弃,沒(méi)有則執(zhí)行:

// /lib/buildChunkGraph.js
for (const chunk of chunkGroup.chunks) {
  const idx = compilation.chunks.indexOf(chunk);
  if (idx >= 0) compilation.chunks.splice(idx, 1); // 刪除 chunk
  chunk.remove('unconnected');
}
chunkGroup.remove('unconnected');

同時(shí)解除 module际邻、chunk世曾、chunkGroup 三者之間的聯(lián)系轮听。

最終每個(gè) module 與每個(gè) chunk椒袍、每個(gè) chunkGroup 之間都建立了聯(lián)系驹暑,優(yōu)化形成了 chunk graph优俘。

此時(shí)的 的 chunk graph

buildChunkGraph 三階段總結(jié):
1.visitModules:為入口模塊和它所有(直接/間接)同步依賴(lài)形成一個(gè) EntryPoint(繼承自 ChunkGroup)惭婿,為所有異步模塊和它的同步依賴(lài)生成一個(gè) chunk 和 chunkGroup(會(huì)重復(fù))财饥。如 chunk 的同步模塊已存在于入口 chunk钥星,則不會(huì)再存入它的_modules谦炒。此階段初始生成了 chunk graph(chunk 依賴(lài)圖)宁改。
2.connectChunkGroups:檢查入口 chunk 和 有異步依賴(lài)的異步 chunk, 如果它們的子 chunk 有它們未包含的新模塊,就建立它們各自所屬 chunkGroup 的 父子關(guān)系秽誊。
3.cleanupUnconnectedGroups:找到?jīng)]有父 chunkgroup 的 chunkgroup讼溺,刪除它里面的 chunk怒坯,并解除與相關(guān) module藻懒、chunk剔猿、chunkGroup 的關(guān)系。
2嬉荆、3 階段對(duì) chunk graph 進(jìn)行了優(yōu)化归敬,去除了 由已存在于入口 chunk 中的 模塊創(chuàng)建的異步 chunk。

回到 Compilation.js鄙早,compilation 的 seal 方法繼續(xù)執(zhí)行汪茧,先將 compilation.modules 按 index 屬性大小排序,然后執(zhí)行:this.hooks.afterChunks.call(this.chunks)限番。觸發(fā)插件 WebAssemblyModulesPlugin:設(shè)置與 webassembly 相關(guān)的報(bào)錯(cuò)信息,到此 chunk 生成結(jié)束。

5.3 module、chunk、chunkGroup 存儲(chǔ)字段相關(guān)

module

module 即每一個(gè)資源文件的模塊對(duì)應(yīng),如 js/css/圖片 等。由 NormalModule 實(shí)例化而來(lái)絮缅,存于compilation.modules數(shù)組吸奴。

  • module.blocks:module 的異步依賴(lài)
  • module.dependencies:module 的同步依賴(lài)
  • module._chunks:module 所屬 chunk 列表
chunk

每一個(gè)輸出文件的對(duì)應(yīng)读处,比如入口文件馆匿、異步加載文件、優(yōu)化切割后的文件等等,存于compilation.chunks數(shù)組。

  • chunk._groups:chunk 所屬的 chunkGroup 列表
  • chunk._modules:由哪些 module 組成
chunkGroup

默認(rèn)情況下,每個(gè) chunkGroup 都只包含一個(gè) chunk:主 chunkGroup (EntryPoint) 包含入口 chunk序臂,其余 chunkGroup 各包含一個(gè)異步模塊 chunk。存于compilation.chunkGroups數(shù)組。
當(dāng)配置了optimization.splitChunksSplitChunksPlugin 插件將入口 chunk 拆分為多個(gè)同步 chunk,那么主 ChunkGroup (EntryPoint) 就會(huì)有多個(gè) chunk 了。另外马绝,如 runtime 被單獨(dú)抽成一個(gè)文件椭赋,那么 EntryPoint 就會(huì)多出一個(gè) runtime chunk。

  • chunkGroup.chunks:由哪些 chunk 組成
  • chunkGroup._blocks:異步依賴(lài) ImportDependenciesBlock
  • chunkGroup._children:子 chunkGroup
  • chunkGroup._parent:父 chunkGroup

下文:淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化

webpack 打包流程系列(未完):
淺析 webpack 打包流程(原理) - 案例 demo
淺析 webpack 打包流程(原理) 一 - 準(zhǔn)備工作
淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
淺析 webpack 打包流程(原理) 三 - 生成 chunk
淺析 webpack 打包流程(原理) 四 - chunk 優(yōu)化
淺析 webpack 打包流程(原理) 五 - 構(gòu)建資源
淺析 webpack 打包流程(原理) 六 - 生成文件

參考鳴謝:
webpack打包原理 ? 看完這篇你就懂了 !
webpack 透視——提高工程化(原理篇)
webpack 透視——提高工程化(實(shí)踐篇)
webpack 4 源碼主流程分析
[萬(wàn)字總結(jié)] 一文吃透 Webpack 核心原理
有點(diǎn)難的 Webpack 知識(shí)點(diǎn):Dependency Graph 深度解析
webpack系列之六chunk圖生成

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市讶迁,隨后出現(xiàn)的幾起案子罚斗,更是在濱河造成了極大的恐慌,老刑警劉巖榕暇,帶你破解...
    沈念sama閱讀 219,366評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件家肯,死亡現(xiàn)場(chǎng)離奇詭異你弦,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)兔仰,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,521評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)嗤堰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人,你說(shuō)我怎么就攤上這事嘱函×梁剑” “怎么了露氮?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,689評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵畜埋,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么埠褪? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,925評(píng)論 1 295
  • 正文 為了忘掉前任浓利,我火速辦了婚禮,結(jié)果婚禮上钞速,老公的妹妹穿的比我還像新娘贷掖。我一直安慰自己,他們只是感情好渴语,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,942評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布苹威。 她就那樣靜靜地躺著,像睡著了一般驾凶。 火紅的嫁衣襯著肌膚如雪牙甫。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,727評(píng)論 1 305
  • 那天调违,我揣著相機(jī)與錄音腹暖,去河邊找鬼。 笑死翰萨,一個(gè)胖子當(dāng)著我的面吹牛脏答,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播亩鬼,決...
    沈念sama閱讀 40,447評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼殖告,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了雳锋?” 一聲冷哼從身側(cè)響起黄绩,我...
    開(kāi)封第一講書(shū)人閱讀 39,349評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎玷过,沒(méi)想到半個(gè)月后爽丹,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,820評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡辛蚊,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,990評(píng)論 3 337
  • 正文 我和宋清朗相戀三年粤蝎,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片袋马。...
    茶點(diǎn)故事閱讀 40,127評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡初澎,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出虑凛,到底是詐尸還是另有隱情碑宴,我是刑警寧澤软啼,帶...
    沈念sama閱讀 35,812評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站延柠,受9級(jí)特大地震影響祸挪,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜贞间,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,471評(píng)論 3 331
  • 文/蒙蒙 一贿条、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧榜跌,春花似錦闪唆、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,017評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至础浮,卻和暖如春帆调,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背豆同。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,142評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工番刊, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人影锈。 一個(gè)月前我還...
    沈念sama閱讀 48,388評(píng)論 3 373
  • 正文 我出身青樓芹务,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親鸭廷。 傳聞我的和親對(duì)象是個(gè)殘疾皇子枣抱,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,066評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容