接上文:淺析 webpack 打包流程(原理) 二 - 遞歸構(gòu)建 module
五裸扶、生成 chunk
生成 chunk 階段概述:在
compilation.finish
回調(diào)中執(zhí)行的 seal 方法中详民,觸發(fā)海量鉤子,就此侵入 webpack 的封包階段;
1.首先對(duì)所有import
和export
做標(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孽锥,包括所有的
NormalModule
和ImportDependenciesBlock
,存儲(chǔ)在一個(gè)blockInfoMap
Map 表當(dāng)中(每一項(xiàng)的值都是它們的直接依賴(lài)细层,同步存 modules惜辑,異步存 blocks)。
以【淺析 webpack 打包流程(原理) - 案例 demo】為例疫赎,得到 blockInfoMap:
看具體數(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),在本例就是:
接下來(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)的是:
- 調(diào)用
addChunkInGroup
為這個(gè)異步 block 創(chuàng)建一個(gè) chunk 和 chunkGroup,同時(shí)建立這兩者之間的聯(lián)系政冻。此時(shí)這個(gè) chunk 是空的,還沒(méi)有添加任何它的依賴(lài)明场; - 把 chunkGroup 添加到
compilation.chunkGroups
(Array) 和compilation.namedChunkGroups
(Map),chunkGroupCounters(計(jì)數(shù) Map)苦锨、blockChunkGroups(映射依賴(lài)和 ChunkGroup 關(guān)系的 Map)趴泌、allCreatedChunkGroups
(收集被創(chuàng)建的 ChunkGroup Set)拉庶。 - 把這項(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; - 更新 queueConnect床佳,建立父 chunkGroup 與新 chunkGroup 的映射;
- 向 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
慨默。
第二階段 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è)模塊更多的圖就容易理解得多了:
這個(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优俘。
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.splitChunks
,SplitChunksPlugin 插件將入口 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圖生成