本文内容基于
webpack 5.74.0
版本进行剖析
前言
- 因为
webpack5
整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于js
文件类型进行剖析,不会对其它类型(css
、image
)进行剖析,所举的例子也都是基于js
类型 - 为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码
- 文章默认读者曾经把握
tapable
、loader
、plugin
等基础知识,对文章中呈现asyncQueue
、tapable
、loader
、plugin
相干代码都会间接展现,不会减少过多阐明 - 因为
webpack5
整体代码过于简单,因而会抽离出外围代码进行剖析解说
外围代码是笔者认为外围代码的局部,必定会造成局部内容(读者也感觉是外围代码)缺失,如果发现缺失局部,请参考其它文章或者私信 / 评论区告知我
文章内容
从 编译入口
->make
->seal
,而后进行seal
阶段整体流程的概述(以流程图和简化代码的模式),而后依据流程图抽离进去的外围模块开展具体的剖析,在剖析过程中,会着重剖析:
Module
、Chunk
、ChunkGroup
、ChunkGraph
之间的关系seal
阶段与make
阶段的区别SplitChunksPlugin
源码的深刻分析
力求可能对简单状况下的 Chunk
构建有一个清晰的理解
1.seal 阶段流程概述
1.1 编译入口 ->make->seal
//node_modules/webpack/lib/webpack.js
const webpack = (options, callback) => {const { compiler, watch, watchOptions} = create(options);
compiler.run();
return compiler;
}
// node_modules/webpack/lib/Compiler.js
class Compiler {run(callback) {const run = () => {this.compile(onCompiled);
}
run();}
compile(callback) {const params = this.newCompilationParams();
this.hooks.beforeCompile.callAsync(params, err => {const compilation = this.newCompilation(params);
this.hooks.make.callAsync(compilation, err => {
compilation.seal(err => {
this.hooks.afterCompile.callAsync(compilation, err => {return callback(null, compilation);
});
});
});
});
}
}
在上一篇文章「Webpack5 源码」make 阶段(流程图)剖析,咱们曾经详细分析其次要模块的代码逻辑:从 entry
入口文件开始,进行依赖门路的 resolve
,而后应用loaders
对文件内容进行转化,最终转化为 AST
找到该入口文件的依赖,而后反复门路解析 resolve
->loaders
对文件内容进行转化 ->AST
找到依赖的流程,最终处理完毕后,会触发 compliation.seal()
流程
1.2 seal 阶段整体概述
create chunks
: 遍历this.entries
,进行多个Chunks
的构建,包含入口文件造成Chunk
、异步依赖造成Chunk
等等optimize
: 对造成的Chunk
进行优化,波及SplitChunkPlgins
插件code generation
: 依据下面的Chunk
造成最终的代码,波及到runtime
以及各种module
代码的生成
seal(callback) {
const chunkGraph = new ChunkGraph(
this.moduleGraph,
this.outputOptions.hashFunction
);
this.chunkGraph = chunkGraph;
//...
this.logger.time("create chunks");
/** @type {Map<Entrypoint, Module[]>} */
for (const [name, { dependencies, includeDependencies, options}] of this.entries) {const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(options);
//...
}
//...
buildChunkGraph(this, chunkGraphInit);
this.logger.timeEnd("create chunks");
this.logger.time("optimize");
//...
while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {/* empty */}
//...
this.logger.timeEnd("optimize");
this.logger.time("code generation");
this.codeGeneration(err => {
//...
this.logger.timeEnd("code generation");
}
}
const buildChunkGraph = (compilation, inputEntrypointsAndModules) => {
// PART ONE
logger.time("visitModules");
visitModules(...);
logger.timeEnd("visitModules");
// PART TWO
logger.time("connectChunkGroups");
connectChunkGroups(...);
logger.timeEnd("connectChunkGroups");
for (const [chunkGroup, chunkGroupInfo] of chunkGroupInfoMap) {for (const chunk of chunkGroup.chunks)
chunk.runtime = mergeRuntime(chunk.runtime, chunkGroupInfo.runtime);
}
// Cleanup work
logger.time("cleanup");
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
logger.timeEnd("cleanup");
};
1.3 seal 阶段整体流程图
1.4 重要概念
Dependency & Module
繁多文件会先构建出 Dependency
,依据类型的不同,会有不同的Dependency
,比方EntryDependency
、ConcatenatedModule
不同类型的 Dependency
能够应用不同的 ModuleFactory
来进行 Dependency
->NormalModule
的转化
一个文件造成的
NormalModule
,除了原始源代码之外,还蕴含许多有意义的信息,例如:应用的loaders
、它的dependencies
、它的exports
等等
下图来自 An in-depth perspective on webpack’s bundling process
Chunk & ChunkGroup & EntryPoint
Chunk
封装一个或者多个 Module
ChunkGroup
由一个或者多个 Chunk
组成,一个 ChunkGroup
能够是其它 ChunkGroup
的parent
或者 child
EntryPoint
是入口类型的ChunkGroup
,蕴含了入口Chunk
下图来自 An in-depth perspective on webpack’s bundling process
ChunkGraph
治理 module、chunk 和 chunkGroup 之间的关系
上面的类图并没有写全属性,只是写上笔者认为重要的属性,上面两个图只是为了更好了解
ChunkGraph
的作用以及治理逻辑,不是作为概括应用
2. 遍历 this.entries,创立 Chunk 和 ChunkGroup
- 进行
new ChunkGraph()
的初始化 - 遍历
this.entries
汇合,依据 name 进行addChunk()
创立一个新的Chunk
,并且创立对应的new Entrypoint()
,也就是ChunkGroup
- 进行一系列对象的存储:
namedChunkGroups
、entrypoints
、chunkGroups
,为后续的逻辑做筹备 - 最初进行 chunk 和 ChunkGroup 的关联:
connectChunkGroupAndChunk()
- 最初进行
this.entries.dependencies
的遍历,因为一个入口Chunk
可能存在多个文件,比方entry: {A: ["1.js", "2.js"]}
,ChunkA
存在1.js
和2.js
,此时的this.entries.dependencies
就是1.js
和2.js
seal() {
const chunkGraph = new ChunkGraph(
this.moduleGraph,
this.outputOptions.hashFunction
);
this.chunkGraph = chunkGraph;
for (const [name, { dependencies, includeDependencies, options}] of this.entries) {
// 1. 获取 chunk 对象
const chunk = this.addChunk(name);
// 2. 依据 options 创立 Entrypoint,entrypoint 为 chunkGroup 对象
const entrypoint = new Entrypoint(options);
// 3. 多个 Map 对象的设置
if (!options.dependOn && !options.runtime) {entrypoint.setRuntimeChunk(chunk); // 前面生成 runtime 代码有用
}
entrypoint.setEntrypointChunk(chunk);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
// 4. 关联 chunkGroup 和 chunk
// const connectChunkGroupAndChunk = (chunkGroup, chunk) => {// if (chunkGroup.pushChunk(chunk)) {// chunk.addGroup(chunkGroup);
// }
// };
connectChunkGroupAndChunk(entrypoint, chunk);
for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {entrypoint.addOrigin(null, { name}, /** @type {any} */(dep).request);
const module = this.moduleGraph.getModule(dep);
if (module) {chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
//...
}
}
}
}
2.1 this.entries
this.entries
是什么?
在触发 hooks.make.tapAsync()
的剖析中,咱们晓得一开始会传入入口文件 entry
,而后应用createDependency()
构建 EntryDependency
,而后调用compilation.addEntry()
开始 make
阶段的执行
// node_modules/webpack/lib/EntryPlugin.js
apply(compiler) {const { entry, options, context} = this;
const dep = EntryPlugin.createDependency(entry, options);
compiler.hooks.make.tapAsync("EntryPlugin", (compilation, callback) => {
compilation.addEntry(context, dep, options, err => {callback(err);
});
});
}
static createDependency(entry, options) {const dep = new EntryDependency(entry);
// TODO webpack 6 remove string option
dep.loc = {name: typeof options === "object" ? options.name : options};
return dep;
}
而在 addEntry()
中:
- 创立
entryData
数据 entryData[target].push(entry)
this.entries.set(name, entryData)
换句话说,this.entries
寄存的就是入口文件类型的 Dependency
数组
// node_modules/webpack/lib/Compilation.js
addEntry(context, entry, optionsOrName, callback) {this._addEntryItem(context, entry, "dependencies", options, callback);
}
_addEntryItem(context, entry, target, options, callback) {const { name} = options;
let entryData =
name !== undefined ? this.entries.get(name) : this.globalEntry;
if (entryData === undefined) {
entryData = {dependencies: [],
includeDependencies: [],
options: {
name: undefined,
...options
}
};
entryData[target].push(entry);
this.entries.set(name, entryData);
} else {entryData[target].push(entry);
//...
}
//...
this.addModuleTree();}
回到文章要剖析的 seal
阶段,咱们就能够晓得,一开始遍历 this.entries
理论就是遍历入口文件,其中 name
是入口文件的名称,dependencies
就是入口文件类型的EntryDependency
,总结起来就是:
在遍历过程中,咱们对每一个入口文件,都调用 addChunk()
进行 Chunk
对象的构建 + 调用 new Entrypoint()
进行 ChunkGroup
对象的构建,而后应用 connectChunkGroupAndChunk()
建设起 ChunkGroup
和Chunk
的关联
seal() {
const chunkGraph = new ChunkGraph(
this.moduleGraph,
this.outputOptions.hashFunction
);
this.chunkGraph = chunkGraph;
for (const [name, { dependencies, includeDependencies, options}] of this.entries) {
// 1. 获取 chunk 对象
const chunk = this.addChunk(name);
// 2. 依据 options 创立 Entrypoint,entrypoint 为 chunkGroup 对象
const entrypoint = new Entrypoint(options);
// 3. 多个 Map 对象的设置
if (!options.dependOn && !options.runtime) {entrypoint.setRuntimeChunk(chunk); // 前面生成 runtime 代码有用
}
entrypoint.setEntrypointChunk(chunk);
this.namedChunkGroups.set(name, entrypoint);
this.entrypoints.set(name, entrypoint);
this.chunkGroups.push(entrypoint);
// 4. 关联 chunkGroup 和 chunk
// const connectChunkGroupAndChunk = (chunkGroup, chunk) => {// if (chunkGroup.pushChunk(chunk)) {// chunk.addGroup(chunkGroup);
// }
// };
connectChunkGroupAndChunk(entrypoint, chunk);
//...
}
}
addChunk(name) {
//name 存在 namedChunks 则返回以后 chunk
if (name) {const chunk = this.namedChunks.get(name);
if (chunk !== undefined) {return chunk;}
}
// 新建 chunk 实例
const chunk = new Chunk(name, this._backCompat);
this.chunks.add(chunk);
if (this._backCompat)
// 增加至 ChunkGraphForChunk Map
ChunkGraph.setChunkGraphForChunk(chunk, this.chunkGraph);
if (name) {
// 增加至 namedChunks Map
this.namedChunks.set(name, chunk);
}
return chunk;
}
2.2 this.entries.dependencies
比方
entry: {A: ["1.js", "2.js"]}
,ChunkA
存在1.js
和2.js
,此时的this.entries.dependencies
就是1.js
和2.js
- 通过
dep
获取对应的NormalModule
,即利用dependency
获取对应的Module 对象
- 应用
chunkGraph.connectChunkAndEntryModule()
关联 chunk、module 和 chunkGroup 的关系 assignDepths()
办法会遍历入口 module 所有的依赖,为每一个 module 设置深度标记
seal() {
const chunkGraph = new ChunkGraph(
this.moduleGraph,
this.outputOptions.hashFunction
);
this.chunkGraph = chunkGraph;
for (const [name, { dependencies, includeDependencies, options}] of this.entries) {// 每一个入口都进行 new Chunk()和 new ChunkGroup()
// 关联 chunkGroup 和 chunk
// 关联 chunk、module、chunkGroup
const entryModules = new Set();
for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {entrypoint.addOrigin(null, { name}, /** @type {any} */(dep).request);
const module = this.moduleGraph.getModule(dep);
if (module) {// const cgm = this._getChunkGraphModule(module);
// const cgc = this._getChunkGraphChunk(chunk);
// if (cgm.entryInChunks === undefined) {// cgm.entryInChunks = new Set();
// }
// cgm.entryInChunks.add(chunk);
// cgc.entryModules.set(module, entrypoint);
chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
entryModules.add(module);
const modulesList = chunkGraphInit.get(entrypoint);
if (modulesList === undefined) {chunkGraphInit.set(entrypoint, [module]);
} else {modulesList.push(module);
}
}
}
// 为 module 设置深度标记
this.assignDepths(entryModules);
}
}
3.buildChunkGraph 概述
从上面代码能够晓得,buildChunkGraph()
次要分为三个局部:
visitModules()
connectChunkGroups()
cleanupUnconnectedGroups
因为每一点的逻辑都比较复杂,因而上面咱们将针对每一个点进行具体的剖析
seal(callback) {
const chunkGraph = new ChunkGraph(
this.moduleGraph,
this.outputOptions.hashFunction
);
this.chunkGraph = chunkGraph;
//...
this.logger.time("create chunks");
/** @type {Map<Entrypoint, Module[]>} */
for (const [name, { dependencies, includeDependencies, options}] of this.entries) {const chunk = this.addChunk(name);
const entrypoint = new Entrypoint(options);
//...
}
//...
buildChunkGraph(this, chunkGraphInit);
//...
}
const buildChunkGraph = (compilation, inputEntrypointsAndModules) => {
// PART ONE
logger.time("visitModules");
visitModules(...);
logger.timeEnd("visitModules");
// PART TWO
logger.time("connectChunkGroups");
connectChunkGroups(...);
logger.timeEnd("connectChunkGroups");
for (const [chunkGroup, chunkGroupInfo] of chunkGroupInfoMap) {for (const chunk of chunkGroup.chunks)
chunk.runtime = mergeRuntime(chunk.runtime, chunkGroupInfo.runtime);
}
// Cleanup work
logger.time("cleanup");
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
logger.timeEnd("cleanup");
};
4.buildChunkGraph-1-visitModules
从上面代码块晓得,visitModules
次要分为三个局部:
inputEntrypointsAndModules
:遍历 inputEntrypointsAndModules,初始化 chunkGroupInfo- 遍历
chunkGroupsForCombining
:解决 chunkGroup 有父 chunkGroup 的状况,将两个 chunkGroupInfo 进行相互关联 - 解决
queue
数据:两个队列,一直循环解决
const visitModules = {for (const [chunkGroup, modules] of inputEntrypointsAndModules) {// 遍历 inputEntrypointsAndModules,初始化 chunkGroupInfo}
for (const chunkGroupInfo of chunkGroupsForCombining) {// 解决 chunkGroup 有父 chunkGroup 的状况,将两个 chunkGroupInfo 进行相互关联}
while (queue.length || queueConnect.size) {processQueue(); // 内层遍历
if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
}
if (queueConnect.size > 0) {processConnectQueue();
if (chunkGroupsForMerging.size > 0) {processChunkGroupsForMerging();
}
}
if (outdatedChunkGroupInfo.size > 0) {processOutdatedChunkGroupInfo();
}
}
}
4.1 visitModules 流程图
4.2 遍历 inputEntrypointsAndModules,初始化 chunkGroupInfo
在下面 2.1
的剖析中,如上面代码所示,咱们会进行 chunkGraphInit
数据结构的初始化,应用 entrypoint
作为 key,将对应入口所蕴含的 Module
都退出到数组中
比方
entry: {A: ["1.js", "2.js"]}
,ChunkA
存在1.js
和2.js
,此时的this.entries.dependencies
就是1.js
和2.js
,chunkGraphInit
依据entrypoint
创立的数组蕴含1.js
和2.js
// node_modules/webpack/lib/Compilation.js
for (const [name, { dependencies, includeDependencies, options}] of this
.entries) {const chunk = this.addChunk(name);
if (options.filename) {chunk.filenameTemplate = options.filename;}
const entrypoint = new Entrypoint(options);
//...
for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {entrypoint.addOrigin(null, { name}, /** @type {any} */(dep).request);
const module = this.moduleGraph.getModule(dep);
if (module) {chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
entryModules.add(module);
const modulesList = chunkGraphInit.get(entrypoint);
if (modulesList === undefined) {chunkGraphInit.set(entrypoint, [module]);
} else {modulesList.push(module);
}
}
}
//...
}
从上面代码能够晓得,咱们会遍历所有 inputEntrypointsAndModules
,获取所有入口文件相干的NormalModule
,而后把它们都退出到queue
中
退出到
queue
之前会判断以后入口文件类型的chunkGroup
是否具备parent
,如果有的话,间接放入chunkGroupsForCombining
,而不放入queue
// 精简代码,只留下要剖析的代码
// inputEntrypointsAndModules = {Entrypoint: [NormalModule] }
// 因为 Entrypoint extends ChunkGroup,因而
// inputEntrypointsAndModules = {ChunkGroup: [NormalModule] }
for (const [chunkGroup, modules] of inputEntrypointsAndModules) {
const runtime = getEntryRuntime(
compilation,
chunkGroup.name,
chunkGroup.options
);
// 为 entry 创立 chunkGroupInfo
const chunkGroupInfo = {
chunkGroup,
runtime,
minAvailableModules: undefined, // 可追踪的最小 module 数量
minAvailableModulesOwned: false,
availableModulesToBeMerged: [],
skippedItems: undefined,
resultingAvailableModules: undefined,
children: undefined,
availableSources: undefined,
availableChildren: undefined
};
if (chunkGroup.getNumberOfParents() > 0) {
// 如果 chunkGroup 有父 chunkGroup,那么可能父 chunkGroup 曾经在其它中央曾经援用它了,须要另外解决
chunkGroupsForCombining.add(chunkGroupInfo);
} else {
chunkGroupInfo.minAvailableModules = EMPTY_SET;
const chunk = chunkGroup.getEntrypointChunk();
for (const module of modules) {
queue.push({
action: ADD_AND_ENTER_MODULE,
block: module,
module,
chunk,
chunkGroup,
chunkGroupInfo
});
}
}
chunkGroupInfoMap.set(chunkGroup, chunkGroupInfo);
if (chunkGroup.name) {namedChunkGroups.set(chunkGroup.name, chunkGroupInfo);
}
}
4.3 检测 chunkGroupsForCombining,解决 EntryPoint 有父 chunkGroup 的状况
遍历 chunkGroupsForCombining
,将两个chunkGroupInfo
进行相互关联,实质就是 availableSources
和availableChildren
相互增加对方chunkGroupInfo
// 解决 chunkGroup 有父 chunkGroup 的状况,将两个 chunkGroupInfo 进行相互关联
for (const chunkGroupInfo of chunkGroupsForCombining) {const { chunkGroup} = chunkGroupInfo;
chunkGroupInfo.availableSources = new Set();
for (const parent of chunkGroup.parentsIterable) {const parentChunkGroupInfo = chunkGroupInfoMap.get(parent);
chunkGroupInfo.availableSources.add(parentChunkGroupInfo);
if (parentChunkGroupInfo.availableChildren === undefined) {parentChunkGroupInfo.availableChildren = new Set();
}
parentChunkGroupInfo.availableChildren.add(chunkGroupInfo);
}
}
4.4 processQueue:解决 queue
将所有入口类型的 module
压入 queue
后,赋予初始状态 ADD_AND_ENTER_MODULE
,而后一直变动状态值,调用不同办法进行解决
从上面 processQueue()
能够晓得,会执行因为几个状态都不存在 break
语句,因而会执行ADD_AND_ENTER_ENTRY_MODULE
->ADD_AND_ENTER_MODULE
->ENTER_MODULE
->PROCESS_BLOCK
for (const [chunkGroup, modules] of inputEntrypointsAndModules) {
// 为 entry 创立 chunkGroupInfo
const chunkGroupInfo = {
chunkGroup,
runtime,
//...
};
chunkGroupInfo.minAvailableModules = EMPTY_SET;
const chunk = chunkGroup.getEntrypointChunk();
for (const module of modules) {
queue.push({
action: ADD_AND_ENTER_MODULE,
block: module,
module,
chunk,
chunkGroup,
chunkGroupInfo
});
}
}
// 取 queue 要 pop(),为了保障拜访程序,须要反转一下数组
queue.reverse();
const processQueue = () => {while (queue.length) {
statProcessedQueueItems++;
const queueItem = queue.pop();
module = queueItem.module;
block = queueItem.block;
chunk = queueItem.chunk;
chunkGroup = queueItem.chunkGroup;
chunkGroupInfo = queueItem.chunkGroupInfo;
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
//...
case ADD_AND_ENTER_MODULE:
//...
case ENTER_MODULE:
//...
case PROCESS_BLOCK: {processBlock(block);
break;
}
case PROCESS_ENTRY_BLOCK: {processEntryBlock(block);
break;
}
case LEAVE_MODULE:
//...
}
}
}
上面将依照
ADD_AND_ENTER_ENTRY_MODULE
->ADD_AND_ENTER_MODULE
->ENTER_MODULE
->PROCESS_BLOCK
程序进行解说
4.4.1 ADD_AND_ENTER_ENTRY_MODULE
取目前的入口 entryModule
,而后进行chunk
、module
、chunkGroup
的关联
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
chunkGraph.connectChunkAndEntryModule(
chunk,
module,
/** @type {Entrypoint} */(chunkGroup)
);
}
// node_modules/webpack/lib/ChunkGraph.js
connectChunkAndEntryModule(chunk, module, entrypoint) {const cgm = this._getChunkGraphModule(module);
const cgc = this._getChunkGraphChunk(chunk);
if (cgm.entryInChunks === undefined) {cgm.entryInChunks = new Set();
}
cgm.entryInChunks.add(chunk);
cgc.entryModules.set(module, entrypoint);
}
4.4.2 ADD_AND_ENTER_MODULE
将 chunk
和module
进行相互关联
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
chunkGraph.connectChunkAndEntryModule(
chunk,
module,
/** @type {Entrypoint} */(chunkGroup)
);
// fallthrough
case ADD_AND_ENTER_MODULE: {if (chunkGraph.isModuleInChunk(module, chunk)) {
// already connected, skip it
break;
}
// We connect Module and Chunk
chunkGraph.connectChunkAndModule(chunk, module);
}
}
// node_modules/webpack/lib/ChunkGraph.js
connectChunkAndModule(chunk, module) {const cgm = this._getChunkGraphModule(module);
const cgc = this._getChunkGraphChunk(chunk);
cgm.chunks.add(chunk);
cgc.modules.add(module);
}
isModuleInChunk(module, chunk) {const cgc = this._getChunkGraphChunk(chunk);
return cgc.modules.has(module);
}
4.4.3 ENTER_MODULE
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
chunkGraph.connectChunkAndEntryModule(
chunk,
module,
/** @type {Entrypoint} */(chunkGroup)
);
// fallthrough
case ADD_AND_ENTER_MODULE: {if (chunkGraph.isModuleInChunk(module, chunk)) {
// already connected, skip it
break;
}
// We connect Module and Chunk
chunkGraph.connectChunkAndModule(chunk, module);
}
case ENTER_MODULE: {const index = chunkGroup.getModulePreOrderIndex(module);
// ... 省略设置 index 的逻辑
queueItem.action = LEAVE_MODULE;
queue.push(queueItem);
}
}
4.4.4 PROCESS_BLOCK
ADD_AND_ENTER_ENTRY_MODULE
->ADD_AND_ENTER_MODULE
->ENTER_MODULE
->PROCESS_BLOCK
,此时会触发 processBlock()
的执行
const processQueue = () => {while (queue.length) {
statProcessedQueueItems++;
const queueItem = queue.pop();
module = queueItem.module;
block = queueItem.block;
chunk = queueItem.chunk;
chunkGroup = queueItem.chunkGroup;
chunkGroupInfo = queueItem.chunkGroupInfo;
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
//...
case ADD_AND_ENTER_MODULE:
//...
case ENTER_MODULE:
//...
case PROCESS_BLOCK: {processBlock(block);
break;
}
case PROCESS_ENTRY_BLOCK: {processEntryBlock(block);
break;
}
case LEAVE_MODULE:
//...
}
}
}
在 processBlock()
中先触发getBlockModules()
同步依赖的
block
=module
,异步依赖就传递不同的参数
const processBlock = block => {const blockModules = getBlockModules(block, chunkGroupInfo.runtime);
}
getBlockModules() {
//... 省略初始化 blockModules 和 blockModulesMap 的逻辑
extractBlockModules(module, moduleGraph, runtime, blockModulesMap);
blockModules = blockModulesMap.get(block);
return blockModules;
}
const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {
//... 省略很多条件判断
for (const connection of moduleGraph.getOutgoingConnections(module)) {
const m = connection.module;
const i = index << 2;
modules[i] = m;
modules[i + 1] = state;
}
//... 省略解决 modules[t]为空的逻辑
// 最终返回的就是 module 所有 import 的依赖 + 对应的 state 的数组
}
moduleGraph.getOutgoingConnections()
是一个看起来十分相熟的办法,在make 阶段
中咱们就遇到过
// node_modules/webpack/lib/ModuleGraph.js
getOutgoingConnections(module) {const connections = this._getModuleGraphModule(module).outgoingConnections;
return connections === undefined ? EMPTY_SET : connections;
}
在 make 阶段
的addModule()
办法执行后,咱们会执行 moduleGraph.setResolvedModule()
,其中会波及到originModule
、dependency
、module
等变量
// node_modules/webpack/lib/Compilation.js
const unsafeCacheableModule =
/** @type {Module & { restoreFromUnsafeCache: Function}} */ (module);
for (let i = 0; i < dependencies.length; i++) {const dependency = dependencies[i];
moduleGraph.setResolvedModule(
connectOrigin ? originModule : null,
dependency,
unsafeCacheableModule
);
unsafeCacheDependencies.set(dependency, unsafeCacheableModule);
}
// node_modules/webpack/lib/ModuleGraph.js
setResolvedModule(originModule, dependency, module) {
const connection = new ModuleGraphConnection(
originModule,
dependency,
module,
undefined,
dependency.weak,
dependency.getCondition(this)
);
const connections = this._getModuleGraphModule(module).incomingConnections;
connections.add(connection);
if (originModule) {const mgm = this._getModuleGraphModule(originModule);
if (mgm._unassignedConnections === undefined) {mgm._unassignedConnections = [];
}
mgm._unassignedConnections.push(connection);
if (mgm.outgoingConnections === undefined) {mgm.outgoingConnections = new SortableSet();
}
mgm.outgoingConnections.add(connection);
} else {this._dependencyMap.set(dependency, connection);
}
}
originModule
: 父 Module,比方上面示例中的 index.jsdependency
: 是父 Module 的依赖汇合,比方上面示例中的"./item/index_item-parent1.js"
,它会在originModule
中产生 4 个dependency
// index.js
import {getC1} from "./item/index_item-parent1.js";
var test = _.add(6, 4) + getC1(1, 3);
var test1 = _.add(6, 4) + getC1(1, 3);
var test2 = getC1(4, 5);
sortedDependencies[0] = {
dependencies: [
{ // HarmonyImportSideEffectDependency
request: "./item/index_item-parent1.js",
userRequest: "./item/index_item-parent1.js"
},
{ // HarmonyImportSpecifierDependency
name: "getC1",
request: "./item/index_item-parent1.js",
userRequest: "./item/index_item-parent1.js"
}
//...
],
originModule: {
userRequest: "/Users/wcbbcc/blog/Frontend-Articles/webpack-debugger/js/src/index.js",
dependencies: [//...10 个依赖,包含下面那两个 Dependency]
}
}
module
: 在make 阶段
中,依赖对象 dependency 会进行 handleModuleCreation(),这个时候触发的是NormalModuleFactory.create()
,会拿出第一个dependencies[0]
,也就是下面示例中的HarmonyImportSideEffectDependency
,也就是import {getC1} from "./item/index_item-parent1.js"
,而后转化为module
// node_modules/webpack/lib/NormalModuleFactory.js
create(data, callback) {const dependencies = /** @type {ModuleDependency[]} */ (data.dependencies);
const dependency = dependencies[0];
const request = dependency.request;
const dependencyType =
(dependencies.length > 0 && dependencies[0].category) || "";
const resolveData = {
request,
dependencies,
dependencyType
};
// 利用 resolveData 进行一系列的 resolve()和 buildModule()操作...
}
回到 processBlock()
的剖析,咱们就能够晓得,connection.module
理论就是以后 module
的所有依赖
其中要记住的是 以后
module
的同步依赖是建设在blockModulesMap.set(block, arr)
的 arr 数组中,此时 block 是以后 module
而以后module
的异步依赖会另外起一个数组 arr,即便blockModulesMap.set(block, arr)
的 block 是以后 module 的异步依赖
const processBlock = block => {const blockModules = getBlockModules(block, chunkGroupInfo.runtime);
}
getBlockModules() {
//... 省略初始化 blockModules 和 blockModulesMap 的逻辑
extractBlockModules(module, moduleGraph, runtime, blockModulesMap);
blockModules = blockModulesMap.get(block);
return blockModules;
}
const extractBlockModules = (module, moduleGraph, runtime, blockModulesMap) => {const queue = [module];
while (queue.length > 0) {const block = queue.pop();
const arr = [];
arrays.push(arr);
blockModulesMap.set(block, arr);
for (const b of block.blocks) {queue.push(b);
}
}
for (const connection of moduleGraph.getOutgoingConnections(module)) {
const m = connection.module;
const i = index << 2;
modules[i] = m;
modules[i + 1] = state;
}
//... 省略解决 modules 去重逻辑
// 最终返回的就是 module 所有 import 的依赖 + 对应的 state 的数组
}
最终 extractBlockModules()
会失去一个依赖数据对象 blockModules
,getBlockModules()
通过以后 module
获取所有的同步依赖,即上面示例中的Array(14)
processBlock()- 解决同步依赖
通过下面的剖析,咱们通过 getBlockModules()
获取以后 block 的所有同步依赖后,咱们对这些依赖进行遍历
同步依赖的
block
=module
,异步依赖就传递不同的参数,如上面的queueBuffer
的数据结构,block
和module
都是同一个数据refModule
次要分为三个方面的解决:
- 如果
activeState
不为 true,则退出到skipConnectionBuffer
汇合中 - 如果
activeState
为 true,然而minAvailableModules
/minAvailableModules
曾经有该 module,也就是 parent chunks 曾经含有该 module,则退出到skipBuffer
汇合中 - 如果可能满足下面两个查看,则把以后的 module 退出到
queueBuffer
中
const processBlock = (block, isSrc) => {const blockModules = getBlockModules(block, chunkGroupInfo.runtime);
for (let i = 0; i < blockModules.length; i += 2) {const refModule = /** @type {Module} */ (blockModules[i]);
if (chunkGraph.isModuleInChunk(refModule, chunk)) {
// skip early if already connected
continue;
}
const activeState = /** @type {ConnectionState} */ (blockModules[i + 1]
);
if (activeState !== true) {skipConnectionBuffer.push([refModule, activeState]);
if (activeState === false) continue;
}
if (
activeState === true &&
(minAvailableModules.has(refModule) ||
minAvailableModules.plus.has(refModule))
) {
// already in parent chunks, skip it for now
skipBuffer.push(refModule);
continue;
}
// enqueue, then add and enter to be in the correct order
// this is relevant with circular dependencies
queueBuffer.push({
action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
block: refModule,
module: refModule,
chunk,
chunkGroup,
chunkGroupInfo
});
}
// 解决 skipConnectionBuffer
// 解决 skipBuffer
// 解决 queueBuffer
}
因为三段逻辑比拟显著和扩散,咱们能够把它们合在一起
如果 activeState
不为 true,则将以后同步依赖退出到 skipConnectionBuffer
汇合中,而后放入到以后 module 的 chunkGroupInfo.skippedModuleConnections
中
for (let i = 0; i < blockModules.length; i += 2) {const activeState = /** @type {ConnectionState} */ (blockModules[i + 1]
);
if (activeState !== true) {skipConnectionBuffer.push([refModule, activeState]);
if (activeState === false) continue;
}
}
if (skipConnectionBuffer.length > 0) {let { skippedModuleConnections} = chunkGroupInfo;
if (skippedModuleConnections === undefined) {
chunkGroupInfo.skippedModuleConnections = skippedModuleConnections =
new Set();}
for (let i = skipConnectionBuffer.length - 1; i >= 0; i--) {skippedModuleConnections.add(skipConnectionBuffer[i]);
}
skipConnectionBuffer.length = 0;
}
如果 activeState
为 true,然而 minAvailableModules
/minAvailableModules
曾经有该 module,也就是 parent chunks 曾经含有该 module,则退出到 skipBuffer
汇合中,而后放入到以后 module 的 chunkGroupInfo.skippedItems
中
for (let i = 0; i < blockModules.length; i += 2) {const activeState = /** @type {ConnectionState} */ (blockModules[i + 1]
);
if (
activeState === true &&
(minAvailableModules.has(refModule) ||
minAvailableModules.plus.has(refModule))
) {
// already in parent chunks, skip it for now
skipBuffer.push(refModule);
continue;
}
}
if (skipBuffer.length > 0) {let {skippedItems} = chunkGroupInfo;
if (skippedItems === undefined) {chunkGroupInfo.skippedItems = skippedItems = new Set();
}
for (let i = skipBuffer.length - 1; i >= 0; i--) {skippedItems.add(skipBuffer[i]);
}
skipBuffer.length = 0;
}
如果可能满足下面两个查看,则把以后的 module 的同步依赖退出到 queueBuffer
中,而后退出到queue
,持续在内层循环中解决同步依赖
for (let i = 0; i < blockModules.length; i += 2) {const activeState = /** @type {ConnectionState} */ (blockModules[i + 1]
);
queueBuffer.push({
action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
block: refModule,
module: refModule,
chunk,
chunkGroup,
chunkGroupInfo
});
}
if (queueBuffer.length > 0) {for (let i = queueBuffer.length - 1; i >= 0; i--) {queue.push(queueBuffer[i]);
}
queueBuffer.length = 0;
}
processBlock()- 解决异步依赖
解决实现同步依赖后,会触发 iteratorBlock(b)
解决以后 module 的异步依赖
从上面的代码块剖析能够晓得,次要分为 3 种状况
-
状况 1: 这个异步依赖 NormalModule 还没有对应的 chunkGroup
- 场景 1:
Entry
类型,压入queueDelayed
,状态置为PROCESS_ENTRY_BLOCK
,构件新的Chunk
- 场景 2:
webpack.config.js
中asyncChunks=false
/chunkLoading=false
,还是应用目前的Chunk
,与同步依赖集成在同一文件中 - 场景 3:
非 Entry
+容许 asyncChunk
的状况,应用addChunkInGroup()
建设新的ChunkGroup
和新的Chunk
,造成新的文件寄存该异步依赖
- 场景 1:
- 状况 2: 这个异步依赖 NormalModule 有对应的 chunkGroup,而且它是入口类型的
- 状况 3: 这个异步依赖 NormalModule 有对应的 chunkGroup,而且它不是入口类型的
最初再进行 Entry 类型和非 Entry 类型的离开解决
const processBlock = (block, isSrc) => {
//... 解决同步依赖
for (const b of block.blocks) {iteratorBlock(b);
}
}
const iteratorBlock = b => {let cgi = blockChunkGroups.get(b);
const entryOptions = b.groupOptions && b.groupOptions.entryOptions;
if (cgi === undefined) {
// 状况 1: 这个异步 NormalModule 还没有对应的 chunkGroup
if (entryOptions) {
// 场景 1: Entry 类型
queueDelayed.push({
action: PROCESS_ENTRY_BLOCK,
block: b,
module: module,
chunk: entrypoint.chunks[0],
chunkGroup: entrypoint,
chunkGroupInfo: cgi
});
} else if (!chunkGroupInfo.asyncChunks || !chunkGroupInfo.chunkLoading) {
// 场景 2: webpack.config.js 中 asyncChunks=false/chunkLoading=false
queue.push({
action: PROCESS_BLOCK,
block: b,
module: module,
chunk,
chunkGroup,
chunkGroupInfo
});
} else {
// 场景 3: 非 Entry+ 容许 asyncChunk 的状况
c = compilation.addChunkInGroup(
b.groupOptions || b.chunkName,
module,
b.loc,
b.request
);
blockConnections.set(b, []);
}
} else if (entryOptions) {
// 状况 2: 这个异步 NormalModule 有对应的 chunkGroup,而且它是入口类型的
entrypoint = cgi.chunkGroup;
} else {
// 状况 3: 这个异步 NormalModule 有对应的 chunkGroup,而且它不是入口类型的
c = cgi.chunkGroup;
}
if (c !== undefined) {// 解决不是 Entry 类型} else if (entrypoint !== undefined) {
// 解决 Entry 类型
chunkGroupInfo.chunkGroup.addAsyncEntrypoint(entrypoint);
}
}
解决不是 Entry 类型:queueConnection 的构建
当 c !== undefined
时,该异步依赖不是 Entry
类型,将它放入到 queueConnection
中
而后把以后异步依赖也放入 queueDelayed
数组中,期待下一次解决,此时咱们要留神,chunkGroup
曾经变为 c
,此时的c
有可能是异步依赖建设的新的ChunkGroup
if (c !== undefined) {blockConnections.get(b).push({
originChunkGroupInfo: chunkGroupInfo,
chunkGroup: c
});
let connectList = queueConnect.get(chunkGroupInfo);
if (connectList === undefined) {connectList = new Set();
queueConnect.set(chunkGroupInfo, connectList);
}
connectList.add(cgi);
// TODO check if this really need to be done for each traversal
// or if it is enough when it's queued when created
// 4. We enqueue the DependenciesBlock for traversal
queueDelayed.push({
action: PROCESS_BLOCK,
block: b,
module: module,
chunk: c.chunks[0],
chunkGroup: c,
chunkGroupInfo: cgi
});
}
processBlock()- 解决异步依赖的异步依赖
存储在 blocksWithNestedBlocks
这个 Set
数据结构中,等到下一个阶段进行解决
const processBlock = (block, isSrc) => {
//... 解决同步依赖
// 解决异步依赖
for (const b of block.blocks) {iteratorBlock(b);
}
if (block.blocks.length > 0 && module !== block) {blocksWithNestedBlocks.add(block);
}
}
在下面的剖析中,咱们晓得当异步依赖是
entry
类型时,咱们会将它退出到queueDelayed
,并且状态置为PROCESS_ENTRY_BLOCK
,那么这个状态执行了什么逻辑呢?
4.4.5 PROCESS_ENTRY_BLOCK
从上面代码能够看出,processEntryBlock()
跟 processBlock()
的整体逻辑是一样的,都是遍历所有同步依赖 blockModules
,而后压入到queueBuffer
中,而后解决异步依赖,而后解决异步依赖的异步依赖
const processEntryBlock = block => {const blockModules = getBlockModules(block, chunkGroupInfo.runtime);
for (let i = 0; i < blockModules.length; i += 2) {const refModule = /** @type {Module} */ (blockModules[i]);
const activeState = /** @type {ConnectionState} */ (blockModules[i + 1]
);
queueBuffer.push({
action:
activeState === true ? ADD_AND_ENTER_ENTRY_MODULE : PROCESS_BLOCK,
block: refModule,
module: refModule,
chunk,
chunkGroup,
chunkGroupInfo
});
}
if (queueBuffer.length > 0) {for (let i = queueBuffer.length - 1; i >= 0; i--) {queue.push(queueBuffer[i]);
}
queueBuffer.length = 0;
}
for (const b of block.blocks) {iteratorBlock(b);
}
if (block.blocks.length > 0 && module !== block) {blocksWithNestedBlocks.add(block);
}
}
4.4.6 LEAVE_MODULE
最初一个状态,设置index
,没有什么特地的逻辑
const processQueue = () => {while (queue.length) {
statProcessedQueueItems++;
const queueItem = queue.pop();
module = queueItem.module;
block = queueItem.block;
chunk = queueItem.chunk;
chunkGroup = queueItem.chunkGroup;
chunkGroupInfo = queueItem.chunkGroupInfo;
switch (queueItem.action) {
case ADD_AND_ENTER_ENTRY_MODULE:
//...
case ADD_AND_ENTER_MODULE:
//...
case ENTER_MODULE:
//...
case PROCESS_BLOCK: {processBlock(block);
break;
}
case PROCESS_ENTRY_BLOCK: {processEntryBlock(block);
break;
}
case LEAVE_MODULE:
const index = chunkGroup.getModulePostOrderIndex(module);
if (index === undefined) {
chunkGroup.setModulePostOrderIndex(
module,
chunkGroupInfo.postOrderIndex++
);
}
if (
moduleGraph.setPostOrderIndexIfUnset(
module,
nextFreeModulePostOrderIndex
)
) {nextFreeModulePostOrderIndex++;}
break;
}
}
}
4.4.7 总结
- 解决同步的依赖 -> 将异步依赖退出队列中 -> 将异步依赖的异步依赖放入到 Set()中
queue
->queueBuffer
(ADD_AND_ENTER_MODULE
)->queueDelayed
(PROCESS_ENTRY_BLOCK
或者PROCESS_BLOCK
)
4.5 解决 chunkGroupsForCombining,即 chunkGroup 有父 chunkGroup 的状况
chunkGroupsForCombining 数据是在哪里增加的?数据结构是怎么的?最初是如何解决的?
在下面 visitModules()
的剖析中,会进行 inputEntrypointsAndModules
遍历,而后抉择压入 queue
解决或者压入 chunkGroupsForCombining
解决,而这些数据,会等到一轮 queue
处理完毕后再进行解决
if (chunkGroup.getNumberOfParents() > 0) {
// minAvailableModules for child entrypoints are unknown yet, set to undefined.
// This means no module is added until other sets are merged into
// this minAvailableModules (by the parent entrypoints)
const skippedItems = new Set();
for (const module of modules) {skippedItems.add(module);
}
chunkGroupInfo.skippedItems = skippedItems;
chunkGroupsForCombining.add(chunkGroupInfo);
} else {for (const module of modules) {
queue.push({
action: ADD_AND_ENTER_MODULE,
block: module,
module,
chunk,
chunkGroup,
chunkGroupInfo
});
}
}
for (const chunkGroupInfo of chunkGroupsForCombining) {const { chunkGroup} = chunkGroupInfo;
chunkGroupInfo.availableSources = new Set();
for (const parent of chunkGroup.parentsIterable) {const parentChunkGroupInfo = chunkGroupInfoMap.get(parent);
chunkGroupInfo.availableSources.add(parentChunkGroupInfo);
if (parentChunkGroupInfo.availableChildren === undefined) {parentChunkGroupInfo.availableChildren = new Set();
}
parentChunkGroupInfo.availableChildren.add(chunkGroupInfo);
}
}
在 processQueue()
的内层循环完结时,咱们会进行 chunkGroupsForCombining
数据的对立解决
每一次遍历完 queue,都会触发一次
chunkGroupsForCombining.size
的检测
while (queue.length || queueConnect.size) {processQueue();
if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
}
//...
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
processChunkGroupsForCombining()
具体逻辑如下所示,波及到一个比拟难懂的办法: calculateResultingAvailableModules()
,咱们临时了解为它能够计算出以后 Chunk
的可复用的最小模块,能够应用一个示例简略了解可复用的最小模块:
- 目前
parentModule
为entry.js
,它有同步依赖a.js
、b.js
、c.js
,异步依赖async_B.js
- 目前异步依赖
async_B.js
能够造成新的Chunk
和ChunkGroup
,它有同步依赖a.js
、b.js
- 因为异步依赖
async_B.js
的加载工夫必定慢于parentModule
的同步依赖,因而异步依赖async_B.js
能够间接复用parentModule
的同步依赖a.js
、b.js
,而不必把a.js
、b.js
打包进去本人的Chunk
而 ChunkGroupInfo.minAvailableModules
就是 a.js
、b.js
的NormalModule
汇合
理分明 minAvailableModules
的概念后,咱们就能够对上面代码进行剖析:
- 遍历以后
ChunkGroupInfo
的所有parent ChunkGroupInfo
,即info.availableSources
,而后计算出它们的resultingAvailableModules
可复用的模块,而后一直合并到以后ChunkGroupInfo
的availableModules
属性中 - 最终进行
ChunkGroupInfo.minAvailableModules
的赋值 - 最终
outdatedChunkGroupInfo
增加目前的ChunkGroupInfo
const processChunkGroupsForCombining = () => {for (const info of chunkGroupsForCombining) {for (const source of info.availableSources) {if (!source.minAvailableModules) {chunkGroupsForCombining.delete(info);
break;
}
}
}
for (const info of chunkGroupsForCombining) {const availableModules = /** @type {ModuleSetPlus} */ (new Set());
availableModules.plus = EMPTY_SET;
const mergeSet = set => {if (set.size > availableModules.plus.size) {for (const item of availableModules.plus) availableModules.add(item);
availableModules.plus = set;
} else {for (const item of set) availableModules.add(item);
}
};
// combine minAvailableModules from all resultingAvailableModules
for (const source of info.availableSources) {
const resultingAvailableModules =
calculateResultingAvailableModules(source);
mergeSet(resultingAvailableModules);
mergeSet(resultingAvailableModules.plus);
}
info.minAvailableModules = availableModules;
info.minAvailableModulesOwned = false;
info.resultingAvailableModules = undefined;
outdatedChunkGroupInfo.add(info);
}
chunkGroupsForCombining.clear();};
4.6 解决 queueConnect 和 chunkGroupsForMerging
queueConnect 数据是在哪里增加的?数据结构是如何?最初是如何解决 queueConnect 这种数据的?
4.6.1 queueConnect 数据增加
在下面的剖析中,咱们能够晓得,解决 NormalModule
的异步依赖时,咱们会触发 iteratorBlock()
办法
在iteratorBlock()
中,咱们会将异步依赖新创建的 ChunkGroup
退出到 queueConnect
中,而后将目前的异步依赖的 action
置为 PROCESS_BLOCK
,从新进行processBlock
的同步依赖和异步依赖的解决
如上面代码块所示,c
实际上是一个非入口类型的 chunkGroup
queueConnect
存储的是:
key
: 以后ChunkGroupInfo
value
: 非入口类型创立的新chunkGroup
汇合数组
// 解决 NormalModule 的异步依赖 b
const iteratorBlock = b => {
// 如果 c 之前不存在,须要从新建设,这里只是为了更好了解而摘出这部分代码
c = compilation.addChunkInGroup(
b.groupOptions || b.chunkName,
module,
b.loc,
b.request
);
c.index = nextChunkGroupIndex++;
if (c !== undefined) {
// b 为非入口的异步依赖
blockConnections.get(b).push({
originChunkGroupInfo: chunkGroupInfo,
chunkGroup: c
});
let connectList = queueConnect.get(chunkGroupInfo);
if (connectList === undefined) {connectList = new Set();
queueConnect.set(chunkGroupInfo, connectList);
}
connectList.add(cgi);
queueDelayed.push({
action: PROCESS_BLOCK,
block: b,
module: module,
chunk: c.chunks[0],
chunkGroup: c,
chunkGroupInfo: cgi
});
} else if (entrypoint !== undefined) {chunkGroupInfo.chunkGroup.addAsyncEntrypoint(entrypoint);
}
}
4.6.2 解决 queueConnect 数据
在 iteratorBlock()
中进行 queueConnect
数据的构建后
在processQueue()
的内层循环完结时,咱们会进行 queueConnect
数据的对立解决
每一次遍历完 queue,都会触发一次
queueConnect.size
的检测
while (queue.length || queueConnect.size) {processQueue();
if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
}
if (queueConnect.size > 0) {
// calculating available modules
processConnectQueue();
if (chunkGroupsForMerging.size > 0) {
// merging available modules
processChunkGroupsForMerging();}
}
//...
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
processConnectQueue()
解决以后 ChunkGroupInfo
的异步依赖,此时
chunkGroupInfo
: 以后的ChunkGroupInfo
targets
:以后的ChunkGroupInfo
的异步依赖中非入口类型新建的ChunkGroup
汇合数组
上面代码整体流程能够概括为:
- 先将非入口类型异步依赖新建的
ChunkGroup
都退出到以后的ChunkGroupInfo.children
中 - 计算出以后的
ChunkGroupInfo
最小可复用的 module 汇合数据,而后增加到新建的ChunkGroup.availableModulesToBeMerged
属性中 - 将非入口类型异步依赖新建的
ChunkGroup
都退出到chunkGroupsForMerging
汇合中,筹备下一个阶段
const processConnectQueue = () => {// 解决异步依赖创立的 <ChunkGroupInfo, chunkGroup[]> 之间的关联
for (const [chunkGroupInfo, targets] of queueConnect) {
// 1. Add new targets to the list of children
for (const target of targets) {chunkGroupInfo.children.add(target);
}
// 2. Calculate resulting available modules
const resultingAvailableModules =
calculateResultingAvailableModules(chunkGroupInfo);
const runtime = chunkGroupInfo.runtime;
// 3. Update chunk group info
for (const target of targets) {target.availableModulesToBeMerged.push(resultingAvailableModules);
chunkGroupsForMerging.add(target);
const oldRuntime = target.runtime;
const newRuntime = mergeRuntime(oldRuntime, runtime);
if (oldRuntime !== newRuntime) {
target.runtime = newRuntime;
outdatedChunkGroupInfo.add(target);
}
}
statConnectedChunkGroups += targets.size;
}
queueConnect.clear();};
4.6.3 解决 chunkGroupsForMerging 数据
在下面调用 processConnectQueue()
解决实现 queueConnect
数据后,会触发 processChunkGroupsForMerging()
解决 chunkGroupsForMergings
数据
while (queue.length || queueConnect.size) {processQueue();
if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
}
if (queueConnect.size > 0) {
// calculating available modules
processConnectQueue();
if (chunkGroupsForMerging.size > 0) {
// merging available modules
processChunkGroupsForMerging();}
}
//...
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
注:因为
processChunkGroupsForMerging()
代码量过多,因而为了简化解决,将应用一个示例解说该办法,并且只保留示例会运行的条件代码
如上图所示,有两个入口会同时持有异步依赖 async_B.js
,在下面processConnectQueue()
的剖析中,咱们能够晓得,应用 calculateResultingAvailableModules()
能够计算出 resultingAvailableModules
为:
entry1.js
:['./src/entry1.js', './item/entry1_a.js', './item/entry1_b.js', './item/common_____g.js']
entry2.js
:['./src/entry2.js', './item/entry1_b.js', './item/entry2_aa', './item/common_____g.js']
而后触发 target.availableModulesToBeMerged.push(resultingAvailableModules)
,会将下面失去的两个数组放入到ChunkGroupInfo.availableModulesToBeMerged
数据中,最终这些数据会带到 processChunkGroupsForMerging()
中
如上面 processChunkGroupsForMerging()
所示,一开始因为 cachedMinAvailableModules
为空,会先赋值一个 resultingAvailableModules
给cachedMinAvailableModules
,而后再开始比拟计算并集
如上面代码正文所示,计算并集的逻辑其实也不难懂,先拿出 cachedMinAvailableModules[i]
,而后比对availableModules
有没有蕴含这个数据,如果没有,则阐明得计算并集,最终触发outdatedChunkGroupInfo.add(info)
,进行下一个阶段的解决
为什么要计算并集其实也很好了解,如咱们下面所剖析那样
entry1.js
能够为async_B.js
一些复用的 module,entry2.js
能够为async_B.js
一些复用的 module
程序会先加载同步依赖(即复用的 module),再加载async_B.js
那么如果async_B.js
外部本人也 import 这些复用的 module 作为同步依赖,那么就不必把这些可复用的 module 打包进去async_B.js
所造成的Chunk
了,因为能够间接应用Parent Chunk
的同步依赖
然而entry1.js
和entry2.js
能够提供复用的 module 有一些是不一样的怎么办?
比方entry1.js
能够提供 a、b、c,entry2.js
能够提供 b、c、d、e,async_B.js
须要的同步依赖是 a、c
因为不分明是先加载哪个入口文件,因而只能计算entry1.js
和entry2.js
提供复用的 module 的并集,也就是 b、c
因而async_B.js
如果须要 b、c,那就不必额定打包了,间接复用即可,然而理论async_B.js
须要的同步依赖是 a、c,因而async_B.js
还得把 a 打包进去
const processChunkGroupsForMerging = () => {for (const info of chunkGroupsForMerging) {
const availableModulesToBeMerged = info.availableModulesToBeMerged;
let cachedMinAvailableModules = info.minAvailableModules;
if (availableModulesToBeMerged.length > 1) {availableModulesToBeMerged.sort(bySetSize);
}
let changed = false;
merge: for (const availableModules of availableModulesToBeMerged) {if (cachedMinAvailableModules === undefined) {
cachedMinAvailableModules = availableModules;
info.minAvailableModules = cachedMinAvailableModules;
info.minAvailableModulesOwned = false;
changed = true;
} else {if (info.minAvailableModulesOwned) {//...} else if (cachedMinAvailableModules.plus === availableModules.plus) {
//...
//!!!计算并集
for (const m of cachedMinAvailableModules) {if (!availableModules.has(m)) {const newSet = /** @type {ModuleSetPlus} */ (new Set());
newSet.plus = availableModules.plus;
const iterator = cachedMinAvailableModules[Symbol.iterator]();
let it;
while (!(it = iterator.next()).done) {
const module = it.value;
if (module === m) break;
newSet.add(module);
}
while (!(it = iterator.next()).done) {
const module = it.value;
if (availableModules.has(module)) {newSet.add(module);
}
}
info.minAvailableModulesOwned = true;
info.minAvailableModules = newSet;
changed = true;
continue merge;
}
}
} else {//...}
}
}
if (changed) {
info.resultingAvailableModules = undefined;
outdatedChunkGroupInfo.add(info);
}
}
chunkGroupsForMerging.clear();};
4.7 解决 outdatedChunkGroupInfo
在经验 processQueue()
->processConnectQueue()
->processChunkGroupsForMerging()
的解决后,最终到 processOutdatedChunkGroupInfo()
的执行
while (queue.length || queueConnect.size) {processQueue();
if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
}
if (queueConnect.size > 0) {
// calculating available modules
processConnectQueue();
if (chunkGroupsForMerging.size > 0) {
// merging available modules
processChunkGroupsForMerging();}
}
if (outdatedChunkGroupInfo.size > 0) {
// check modules for revisit
processOutdatedChunkGroupInfo();}
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
processOutdatedChunkGroupInfo()
的代码也很多,然而逻辑是比拟清晰易懂的,如上面所示,分为 4 个局部,因为以后异步依赖 ChunkGroupInfo
的minAvailableModules
产生了变动,导致之前解决的一些逻辑都得从新查看一遍,次要包含:
skippedItems
: 之前因为检测到minAvailableModules
蕴含以后 module,即Parent Chunks
能够提供以后 module 进行复用,因而没有退出到queue
中进行解决,当初从新检测了下,这些跳过的 module 是否还在minAvailableModules
中,如果没有,则须要重新加入队列中进行解决skippedModuleConnections
:之前因为检测到activeState
不为 true,因而退出到skippedModuleConnections
,当初从新检测下状态是否产生扭转,如果产生扭转,则须要重新加入队列中进行解决children chunk groups
:从新将children chunk
退出到queueConnect
中,也就是须要计算下异步依赖的minAvailableModules
,因为异步依赖的minAvailableModules
是依靠于parent chunk
,当初parent chunk
的minAvailableModules
产生扭转,对应的异步依赖也同样须要从新计算下minAvailableModules
availableChildren
: 拿出以后ChunkGroup
的子ChunkGroup
,将children
都重新加入到chunkGroupsForCombining
从新计算下minAvailableModules
const processOutdatedChunkGroupInfo = () => {
statChunkGroupInfoUpdated += outdatedChunkGroupInfo.size;
// Revisit skipped elements
for (const info of outdatedChunkGroupInfo) {
// 1. Reconsider skipped items
if (info.skippedItems !== undefined) {const { minAvailableModules} = info;
for (const module of info.skippedItems) {
if (!minAvailableModules.has(module) &&
!minAvailableModules.plus.has(module)
) {
queue.push({
action: ADD_AND_ENTER_MODULE,
block: module,
module,
chunk: info.chunkGroup.chunks[0],
chunkGroup: info.chunkGroup,
chunkGroupInfo: info
});
info.skippedItems.delete(module);
}
}
}
// 2. Reconsider skipped connections
if (info.skippedModuleConnections !== undefined) {const { minAvailableModules} = info;
for (const entry of info.skippedModuleConnections) {const [module, activeState] = entry;
if (activeState === false) continue;
if (activeState === true) {info.skippedModuleConnections.delete(entry);
}
if (
activeState === true &&
(minAvailableModules.has(module) ||
minAvailableModules.plus.has(module))
) {info.skippedItems.add(module);
continue;
}
queue.push({
action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
block: module,
module,
chunk: info.chunkGroup.chunks[0],
chunkGroup: info.chunkGroup,
chunkGroupInfo: info
});
}
}
// 2. Reconsider children chunk groups
if (info.children !== undefined) {
statChildChunkGroupsReconnected += info.children.size;
for (const cgi of info.children) {let connectList = queueConnect.get(info);
if (connectList === undefined) {connectList = new Set();
queueConnect.set(info, connectList);
}
connectList.add(cgi);
}
}
// 3. Reconsider chunk groups for combining
if (info.availableChildren !== undefined) {for (const cgi of info.availableChildren) {chunkGroupsForCombining.add(cgi);
}
}
}
outdatedChunkGroupInfo.clear();};
4.8 calculateResultingAvailableModules 详解
4.8.1 源码剖析
在下面的流程中,咱们屡次应用到 calculateResultingAvailableModules()
这个办法,它自身的代码量也很少,逻辑方面也十分直白,次要是两个公式的计算,次要是 minAvailableModules 和 minAvailableModules.plus 的比拟
resultingAvailableModules 分为两个局部
- resultingAvailableModules = new Set():modules of chunk
- resultingAvailableModules.plus = new Set():比拟 minAvailableModules/minAvailableModules.plus
当 minAvailableModules 的长度 <=minAvailableModules.plus 的长度时,维持 plus 不变,将 minAvailableModules
并入到 resultingAvailableModules
中
当 minAvailableModules 的长度 >minAvailableModules.plus 的长度,此时 plus 须要裁减,将 minAvailableModules
并入到 resultingAvailableModules.plus
中
因而最终的后果就是
- resultingAvailableModules = (modules of chunk) + (minAvailableModules + minAvailableModules.plus)
- resultingAvailableModules = (minAvailableModules + modules of chunk) + (minAvailableModules.plus)
惟一区别就是 minAvailableModules 到底是放在 resultingAvailableModules 还是 resultingAvailableModules.plus
const calculateResultingAvailableModules = chunkGroupInfo => {if (chunkGroupInfo.resultingAvailableModules)
return chunkGroupInfo.resultingAvailableModules;
const minAvailableModules = chunkGroupInfo.minAvailableModules;
// Create a new Set of available modules at this point
// We want to be as lazy as possible. There are multiple ways doing this:
// Note that resultingAvailableModules is stored as "(a) + (b)" as it's a ModuleSetPlus
// - resultingAvailableModules = (modules of chunk) + (minAvailableModules + minAvailableModules.plus)
// - resultingAvailableModules = (minAvailableModules + modules of chunk) + (minAvailableModules.plus)
// We choose one depending on the size of minAvailableModules vs minAvailableModules.plus
let resultingAvailableModules;
if (minAvailableModules.size > minAvailableModules.plus.size) {// resultingAvailableModules = (modules of chunk) + (minAvailableModules + minAvailableModules.plus)
resultingAvailableModules =
/** @type {Set<Module> & {plus: Set<Module>}} */ (new Set());
for (const module of minAvailableModules.plus)
minAvailableModules.add(module);
minAvailableModules.plus = EMPTY_SET;
resultingAvailableModules.plus = minAvailableModules;
chunkGroupInfo.minAvailableModulesOwned = false;
} else {// resultingAvailableModules = (minAvailableModules + modules of chunk) + (minAvailableModules.plus)
resultingAvailableModules =
/** @type {Set<Module> & {plus: Set<Module>}} */ (new Set(minAvailableModules)
);
resultingAvailableModules.plus = minAvailableModules.plus;
}
// add the modules from the chunk group to the set
for (const chunk of chunkGroupInfo.chunkGroup.chunks) {for (const m of chunkGraph.getChunkModulesIterable(chunk)) {resultingAvailableModules.add(m);
}
}
return (chunkGroupInfo.resultingAvailableModules =
resultingAvailableModules);
}
4.8.2 示例图解
5.buildChunkGraph-2-connectChunkGroups
5.1 blockConnections 数据收集
blockConnections
数据在 iteratorBlock()
解决异步依赖时初始化
解决不是 Entry 类型:queueConnection 的构建
const processBlock = (block, isSrc) => {
//... 解决同步依赖
for (const b of block.blocks) {iteratorBlock(b);
}
}
const iteratorBlock = b => {if (c !== undefined) {blockConnections.get(b).push({
originChunkGroupInfo: chunkGroupInfo,
chunkGroup: c
});
let connectList = queueConnect.get(chunkGroupInfo);
if (connectList === undefined) {connectList = new Set();
queueConnect.set(chunkGroupInfo, connectList);
}
connectList.add(cgi);
// TODO check if this really need to be done for each traversal
// or if it is enough when it's queued when created
// 4. We enqueue the DependenciesBlock for traversal
queueDelayed.push({
action: PROCESS_BLOCK,
block: b,
module: module,
chunk: c.chunks[0],
chunkGroup: c,
chunkGroupInfo: cgi
});
}
}
5.2 解决 blockConnections 数据,绑定 ChunkGroup
如上面代码块所示,areModulesAvailable()
次要是判断该异步的 chunkGroup
所有的依赖是否都处于 parent chunkGroup
的resultingAvailableModules
中,也就是 parent chunkGroup
的一些同步依赖曾经蕴含了异步依赖所须要的所有modules
异步依赖间接拿
parent chunkGroup
的同步依赖即可,不须要跟其余module
建设关系
connectBlockAndChunkGroup()
: 异步依赖 AsyncDependenciesBlock 跟新建设的 ChunkGroup 进行绑定connectChunkGroupParentAndChild()
: 异步依赖 ChunkGroup 跟其 parent ChunkGroup 进行绑定
const connectChunkGroups = (compilation, blocksWithNestedBlocks, blockConnections, chunkGroupInfoMap) => {const { chunkGraph} = compilation;
// 呈现在父 chunkA 有异步依赖 chunkB,chunkB 有同步依赖 chunkC
// 然而 chunkC 是 chunkA 的同步依赖,那么 chunkB 就跳过这个异步 chunkC 的关联
for (const [block, connections] of blockConnections) {
if (!blocksWithNestedBlocks.has(block) &&
connections.every(({chunkGroup, originChunkGroupInfo}) =>
// originChunkGroupInfo 蕴含了这个 chunkGroup 的所有 Modules
// 阐明异步依赖 block 所在的 chunk 曾经被所在的 chunk 的父 chunk 蕴含了
areModulesAvailable(
chunkGroup,
originChunkGroupInfo.resultingAvailableModules
)
)
) {continue;}
for (let i = 0; i < connections.length; i++) {const { chunkGroup, originChunkGroupInfo} = connections[i];
// 关联这个 AsyncDependenciesBlock 和 chunkGroup
chunkGraph.connectBlockAndChunkGroup(block, chunkGroup);
// 关联这个 chunkGroup 和它的父 chunkGroup
connectChunkGroupParentAndChild(originChunkGroupInfo.chunkGroup, chunkGroup);
}
}
};
下面的剖析可能看起来有点懵,然而举一个具体的例子就能很快明确 connectChunkGroups()
的逻辑,如上面所示
- 如果
entry1.js
没有同步依赖async_B.js
,那么因为它有异步依赖async_B.js
,async_B.js
会独自造成一个Chunk
和ChunkGroup
- 然而当初
entry1.js
曾经有了同步依赖async_B.js
,那么它就没必要再让async_B.js
独自造成一个Chunk
和ChunkGroup
,因为entry1.js
曾经把async_B.js
打包进去本人的Chunk
了,而下面代码中areModulesAvailable()
就是检测这个逻辑的具体方法,如果originChunkGroupInfo 蕴含了这个 chunkGroup 的所有 Modules
,那么这个异步ChunkGroup
就能够删除了
具体删除逻辑请看下一节的剖析
6.buildChunkGraph-3-cleanupUnconnectedGroups
革除所有没有连贯的
chunkGroups
6.1 allCreatedChunkGroups 数据收集
allCreatedChunkGroups
也是在解决异步依赖 iteratorBlock()
中进行数据初始化
const processBlock = (block, isSrc) => {
//... 解决同步依赖
for (const b of block.blocks) {iteratorBlock(b);
}
}
const iteratorBlock = b => {let cgi = blockChunkGroups.get(b);
const entryOptions = b.groupOptions && b.groupOptions.entryOptions;
if (cgi === undefined) {
// 状况 1: 这个异步 NormalModule 还没有对应的 chunkGroup
if (entryOptions) {// 场景 1: Entry 类型} else if (!chunkGroupInfo.asyncChunks || !chunkGroupInfo.chunkLoading) {// 场景 2: webpack.config.js 中 asyncChunks=false/chunkLoading=false} else {
// 场景 3: 非 Entry+ 容许 asyncChunk 的状况
c = compilation.addChunkInGroup(
b.groupOptions || b.chunkName,
module,
b.loc,
b.request
);
blockConnections.set(b, []);
allCreatedChunkGroups.add(c);
}
} else if (entryOptions) {
// 状况 2: 这个异步 NormalModule 有对应的 chunkGroup,而且它是入口类型的
entrypoint = cgi.chunkGroup;
} else {
// 状况 3: 这个异步 NormalModule 有对应的 chunkGroup,而且它不是入口类型的
c = cgi.chunkGroup;
}
if (c !== undefined) {// 解决不是 Entry 类型} else if (entrypoint !== undefined) {
// 解决 Entry 类型
chunkGroupInfo.chunkGroup.addAsyncEntrypoint(entrypoint);
}
}
6.2 allCreatedChunkGroups 数据处理
通过 chunkGroup.getNumberOfParents()
检测异步 ChunkGroup
是否没有关联其Parent Chunk
,如果没有关联,间接革除该ChunkGroup
const cleanupUnconnectedGroups = (compilation, allCreatedChunkGroups) => {const { chunkGraph} = compilation;
for (const chunkGroup of allCreatedChunkGroups) {
// 清理依赖,如果这个 chunkGroup 的父 chunk 为 0,阐明没有连贯,间接革除
if (chunkGroup.getNumberOfParents() === 0) {for (const chunk of chunkGroup.chunks) {compilation.chunks.delete(chunk);
chunkGraph.disconnectChunk(chunk);
}
chunkGraph.disconnectChunkGroup(chunkGroup);
chunkGroup.remove();}
}
};
如上面所示,当 entry1.js
曾经有了同步依赖 async_B.js
,那么它就没必要再让async_B.js
独自造成一个 Chunk
和ChunkGroup
,因而在下面 connectChunkGroups()
中不会进行 connectChunkGroupParentAndChild(originChunkGroupInfo.chunkGroup, chunkGroup)
关联 ChunkGroup
之间的关系,因而会导致异步依赖 async_B.js
对应的 ChunkGroup.getNumberOfParents() === 0
,最终触发ChunkGroup
删除逻辑,移除该ChunkGroup
7.hooks.optimizeChunks
while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {/* empty */}
在通过 visitModules()
解决后,会调用 hooks.optimizeChunks.call()
进行 chunks
的优化,如下图所示,会触发多个 Plugin
执行,其中咱们最相熟的就是 SplitChunksPlugin
插件
因为篇幅起因,具体分析请看下一篇文章《「Webpack5 源码」seal 阶段剖析(二)》
参考
- 精通 Webpack 外围原理专栏
- webpack@4.46.0 源码剖析 专栏
- webpack5 源码详解 – 封装模块
其它工程化文章
- 「Webpack5 源码」热更新 HRM 流程浅析
- 「Webpack5 源码」make 阶段(流程图)剖析
- 「Webpack5 源码」enhanced-resolve 门路解析库源码剖析
- 「vite4 源码」dev 模式整体流程浅析(一)
- 「vite4 源码」dev 模式整体流程浅析(二)