本文内容基于webpack 5.74.0
版本进行分析
webpack5核心流程
专栏共有5篇,使用流程图的形式分析了webpack5的构建原理
:
- 「Webpack5源码」make阶段(流程图)分析
- 「Webpack5源码」enhanced-resolve路径解析库源码分析
- 「Webpack5源码」seal阶段(流程图)分析(一)
- 「Webpack5源码」seal阶段分析(二)-SplitChunksPlugin源码
- 「Webpack5源码」seal阶段分析(三)-生成代码&runtime
前言
- 由于
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} */
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} */
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