通过一个demo带你深入进入webpack@4.46.0
源码的世界,分析构建原理,专栏地址,共有十篇。
factory.create -> module.build -> runLoaders -> this.parser.parse
完成了单个模块的构建并收集了该模块的依赖存储在module.dependencies中,在Compilation.js中通过afterBuild接着构架依赖的模块。 就这样从entry开始通过dependencies的配合完成了整条依赖链上所有资源的构建,构建出的模块存储在compilation.modules属性上。
由于这个时候每个module都完成了构建并且经过loaders处理得到了_source,如果此时将文件输出,会有很多文件,显然对于web场景下不是最优的,因此webpack提供了一些方式可以进行模块的聚合
(按照一定规则将模块进行组合,最终输出的文件是基于该组合后的内容,组合的内容在内部是通过Chunk
这个类来承载,表示若干个模块组成了一个块
)。 这个聚合
的过程在compilation.seal中
聚合的过程可以分为两类:
初始chunks
优化chunks
(当然这个过程不是必备的)compilation.seal除了包含聚合
的逻辑,还有生成最终内容(构建产物的内容)的逻辑。
compilation.seal方法中的包含大量的钩子,可以参考compilation.seal 中涉及的钩子和函数调用 ,捋完之后实际内置有订阅的插件并不多,遇事不要慌慢慢捋,😄。
在正式进入下面分析之前,先回顾下之前(hooks.make)的成果
可以认为一个dependency graph描述了从初始Dependency(如这里的SingleEntryDependency
)开始直到所有依赖的模块的关系,具有request的XxxDependency
说明关联一个资源因此会被构建出一个模块,最终该dependency和该模块会被关联起来。
后面初步聚合的工作的主要依据就是上面的dependecy graph.
总共三个模块,这里的入口只有一个,即webpack.config.js
中配置的entry: src/simple/main.js。
// Compilation.js
seal(callback) {
... // 各种hooks
for (const preparedEntrypoint of this._preparedEntrypoints) {
const module = preparedEntrypoint.module; // 获取入口模块
const name = preparedEntrypoint.name; // 入口名称:entry中的key
const chunk = this.addChunk(name); // 创建chunk
const entrypoint = new Entrypoint(name); // 创建entryPoint(特殊的chunkGroup,表示来自entry)
entrypoint.setRuntimeChunk(chunk); // 设置运行时所在的chunk
// ...
this.namedChunkGroups.set(name, entrypoint); // 缓存映射关系
this.entrypoints.set(name, entrypoint); // 保存所有的 entryPoint
this.chunkGroups.push(entrypoint); // 保存所有的chunkGroup
GraphHelpers.connectChunkGroupAndChunk(entrypoint, chunk); // 链接chunkGroup和chunk
GraphHelpers.connectChunkAndModule(chunk, module); // 链接chunk和module
chunk.entryModule = module; // 给当前Chunk设置入口模块
chunk.name = name;
//...
}
buildChunkGraph(this, (this.chunkGroups.slice()));
... // 各种hooks
}
compilation.js中addEntry
方法中会将入口模块(NormalModule)保存到_preparedEntrypoints
,取出module,并创建一个新的Chunk
(name = 'chunkMain')和EntryPoint
(options.name = 'chunkMain')对象(EntryPoint
继承自ChunkGroup
),通过GraphHelpers
工具类将容器
与容器item
进行连接,如ChunkGroup
和Chunk
,Chunk
和Module
的关系就是容器与容器item,连接的好处是可以通过容器找到其包含的所有items,也可以通过item查找其归属的容器。
entry: {
chunkMain: './src/simple/main.js',
},
上面创建的Chunk
和EntryPoint
都有一个name,等于上面的chunkMain。后面我们会通过Chunk(name=xxx)
和ChunkGroup(options.name=xxx)
这种形式来区分不同的Chunk和ChunkGroup对象
EntryPoint
相比ChunkGroup
,提供了处理runtimeChunk
的能力,后面在代码生成阶段会看到这部分能力。
class Entrypoint extends ChunkGroup {
// constructor
isInitial() // 返回true,表示是初始chunkGroup
setRuntimeChunk(chunk) // 设置运行时代码应该跟随哪一个chunk
getRuntimeChunk() {..} //
replaceChunk(oldChunk, newChunk) {..} // 替换runtimeChunk
}
所以看到上面for循环中有给EntryPoint
设置runtimeChunk
属性,因为每个EntryPoint
最终输出的文件中需要包含运行时
(可以认为是内置模块化机制-兼容各种模块化规范),后面代码输出阶段可以看到runtimeChunk
的作用。
下面深入看下buildChunkGraph
// inputChunkGroups: [EntryPoint]
const buildChunkGraph = (compilation, inputChunkGroups) => {
// 共享变量定义
/** @type {Map<AsyncDepsendenciesBlock, BlockChunkGroupConnection[]>} */
// 注意:AsyncDepsendenciesBlock
// BlockChunkGroupConnection: [{ originChunkGroupInfo, chunkGroup }]
const blockConnections = new Map();
// 新创建的chunkGroup
const allCreatedChunkGroups = new Set();
// chunkGroup的信息
const chunkGroupInfoMap = new Map();
/** @type {Set<DependenciesBlock>} */
const blocksWithNestedBlocks = new Set(); // 暂时不确定用途❓
// 步骤1:分析chunk应该包含的模块(可能会创建新的chunk,chunkgroup)
visitModules(compilation, inputChunkGroups, chunkGroupInfoMap,
blockConnections, blocksWithNestedBlocks, allCreatedChunkGroups);
// 步骤2:链接
connectChunkGroups(blocksWithNestedBlocks, blockConnections, chunkGroupInfoMap);
// 步骤3:清理
cleanupUnconnectedGroups(compilation, allCreatedChunkGroups);
}
步骤如下:
chunk graph
这里重点介绍step1即visitModules
这个函数的作用是初步确定最终会有哪些Chunk
,每个Chunk
分别包含哪些Module
;在具体分析这段逻辑之前,先通过一个小案例来直观的感受下这段代码作用后的结果
buildChunkGraph
之前的for循环中创建了EntryPoint
(也是一个ChunkGroup),EntryPoint
关联了一个Chunk
,这个Chunk
关联了一个entryModule
。那么这个Chunk
最终需要包含的所有Module
都是由这个entryModule
及其依赖链上的所有模块组成的,其依赖链上存在同步模块也存在异步模块,对于异步模块是可以拆分成一个单独ChunkGroup
感性认识
一下这部分做的事情的例子:
EntryPoint
是上面for循环得到的,右侧ChunkGroup
是新生成的(由异步引入import('xx')特性引发,即异步引入一个模块时会创建一个新ChunkGroup
)EntryPoint
包含一个Chunk,这个Chunk包含了entryModule
依赖链上的所有同步模块
ChunkGroup
的考虑:尽可能使得首屏加载的代码量(文件/模块)少,对首屏不需要的进行懒加载,以达到优化首屏渲染时间的目的。
ChunkGroupInfo
中部分介绍
标题 | |
---|---|
minAvailableModules | 假设一个异步引入的模块同时被entryA和entryB异步引用,由于异步引用的模块会被拆分出来作为单独的文件(Chunk)加载,webpack认为这个单独的文件可以复用 entryA和entryB的公共(同步)模块,这是合理的,因为会加载完entryA或entryB才会加载这个单独的文件,此时可以复用的最小模块集合就是entryA和entryB的交集(同步)模块 |
skippedItems | 如果异步模块同步引入的模块在minAvailableModules中已经有了,显然就可以跳过,因为在父Chunk中已经包含了,子Chunk没必要重复包含,因此跳过 |
minAvailableModulesOwned, resultingAvailableModules, availableModulesToBeMerged | 计算交集的临时变量 |
children | ChunkGroup有父子关系,表示该chunkGroup的子ChunkGroup |
visitModuels
虽是一个方法,但是却很复杂,当前版本这一个方法400+行。核心逻辑本来是可以按照递归的方式写的,但是由于递归可能会有栈溢出问题,所以作者改为了非递归的形式出现,因此大量的变量在这里声明来辅助非递归实现,此外部分关键变量(如queue
、blockInfoMap
)需要完成初始化操作;由于递归本身构成了函数栈,因此改写成非递归形式后需要借助数据结构栈
来模拟函数栈的效果,此外是少不了循环的。这里改写成了栈(对应变量queue,实际是栈的功能push/pop) + while的形式;
// while()前面的注释 // Iterative traversal of the Module graph // Recursive would be simpler to write but could result in Stack Overflows
该方法的核心逻辑如下
const visitModules = (compilation, inputChunkGroups, chunkGroupInfoMap, blockConnections, blocksWithNestedBlocks, allCreatedChunkGroups) => {
// 1. 变量声明和初始化
// Iterative traversal of the Module graph
// Recursive would be simpler to write but could result in Stack Overflows
while (queue.length) {
// 2. 处理依赖链上的同步依赖模块,延迟处理异步依赖block
while (queue.length) {/*...*/}
// 3. 处理 minAvailableModules 收缩的场景
while (queueConnect.size > 0) {/*...*/}
// 4. 处理异步依赖 block
if (queue.length === 0) {/*...*/}
}
}
总共三个while,外层while,内层第一个while,内层第二个while,下面通过此类描述来指向相关代码片段。
两个重要的变量初始化
const blockInfoMap = extraceBlockInfoMap(compilation);
// extraceBlockInfoMap 核心逻辑
for (const module of compilation.modules) {
blockQueue = [module];
currentModule = module;
while (blockQueue.length > 0) {
block = blockQueue.pop();
// 重新初始化blockInfoModules、blockInfoBlocks
blockInfoModules = new Set();
blockInfoBlocks = [];
//...
if (block.dependencies) {
// 从dep中解析出关联的module,并保存到blockInfoModules中
for (const dep of block.dependencies) iteratorDependency(dep);
}
if (block.blocks) {
// iteratorBlockPrepare将block保存到blockInfoBlocks,并且
// push到blockQueue,在下个循环中,会收集该block的信息
for (const b of block.blocks) iteratorBlockPrepare(b);
}
const blockInfo = {
modules: blockInfoModules,
blocks: blockInfoBlocks
};
blockInfoMap.set(block, blockInfo);
}
blockInfoMap
的key
是DependenciesBlock
类型,保存每个blcok的同步依赖模块到modules
属性(Module
的子类如NormalModule
)和异步依赖block到blocks
属性(AsyncDependenciesBlock
的子类ImportDependenciesBlock
)
extraceBlockInfoMap
遍历compilation.modules
,这些modules只是遍历的起点,遇到AsyncDependenciesBlock
(即block.blocks)会push到blockQueue
中,下一次循环的时候会针对这些异步block
做一次信息收集(收集每个block
的modules和blocks),所以最终从compilation.modules
到其依赖链
上的所有block
都会被收集到。
案例中的blockInfoMap
的初始值如下(一共四个module):
key(Module类型) | value.modules(同步依赖, Module类型) | value.blocks(异步依赖, AsynDependenciesBlock类型) |
---|---|---|
NormalModule (rawRequest= './src/simple/main.js') | NormalModule (rawRequest = './custom-loaders/custom-inline-loader.js??share-opts!./a?c=d') | NormalModule (rawRequest = './b') |
NormalModule (rawRequest = './custom-loaders/custom-inline-loader.js??share-opts!./a?c=d') | NormalModule (rawRequest= './c') | |
NormalModule (rawRequest = './b') | | |
NormalModule (rawRequest= './c') | | |
const reduceChunkGroupToQueueItem = (queue, chunkGroup) => {
for (const chunk of chunkGroup.chunks) {
const module = chunk.entryModule;
queue.push({
action: ENTER_MODULE,
block: module,
module,
chunk,
chunkGroup
});
}
chunkGroupInfoMap.set(chunkGroup, {
chunkGroup,
minAvailableModules: new Set(),
minAvailableModulesOwned: true,
availableModulesToBeMerged: [],
skippedItems: [],
resultingAvailableModules: undefined,
children: undefined
});
return queue;
};
let queue = inputChunkGroups
.reduce(reduceChunkGroupToQueueItem, [])
.reverse();
inputChunkGroups
就是初始构造的所有EntryPoint
,从Chunk中取出entryModule
,并构造QueueItem
,此时block和module指向同一个对象,都是Module类型,关注一下action
的默认值ENTER_MODULE
。
另外这里会给初始的EntryPoint
构造默认的ChunkGroupInfo
。
let module, chunk, chunkGroup, chunkGroupInfo, block, minAvailableModules, skippedItems;
后面的while
会先queue.pop()
弹出QueueItem
从中取出相应属性赋值给变量module
, block
, chunk
, chunkGroup
,从chunkGroupInfoMap
获取chunkGroupInfo
,然后拿到属性minAvailableModules
, skippedItems
并赋值给上面的变量。
声明为函数作用域变量的好处:
重点关注下面第二个部分
//...
while (queue.length) {
// 2. 处理依赖链上的同步依赖模块,延迟处理异步依赖block
while (queue.length) {
const queueItem = queue.pop();
// 变量赋值:module、block、chunk、chunkGroup、chunkGroupInfo、minAvailableModules、skippedItems
switch (queueItem.action) {
case ADD_AND_ENTER_MODULE: {/*..*/} // fallthrough, 没有break
case ENTER_MODULE: {/*..*/} // fallthrough, 没有break
case PROCESS_BLOCK: {/*..*/ break; }
case LEAVE_MODULE: {/*..*/ break; }
}
}
//...
}
这里的核心逻辑看到就是switch-case
的各分支逻辑,实际上这几个分支不是独立的,构成了一个流程。 一个模块(Module)的完整流程如下:
ADD_AND_ENTER_MODULE
-> ENTER_MODULE
-> PROCESS_BLOCK
-> LEAVE_MODULE
下面我们具体看下流程中的每个节点都做了些什么操作。
if (minAvailableModules.has(module)) {
// 父chunk已经包含过,这里先在跳过它,
// 但是当 minAvailableModules 缩小时会重新检查(后面会给出具体的案例)
skippedItems.push(queueItem);
break;
}
// 建立chunk和module的连接
if (chunk.addModule(module)) {
module.addChunk(chunk);
} else { // 如果已经包含了,则跳过
break;
}
minAvailableModules
包含过该模块,则暂时先跳过即保存到skippedItems
,如果minAvailableModules
缩小了,则skippedItems
中这些曾经跳过的模块需要重新跑流程。ENTER_MODULE
& LEAVE_MODULE
共同的能力是设置模块自身以及模块在当前chunkGroup
中的index/index2。index/index2包含了顺序的信息,比如设置在chunkGroup
中的index,表示该模块在chunkGroup
自上而下的顺序,而index2则表示自下而上的顺序,比如在官方测试demo中就有使用到如v4.46.0/test/configCases/chunk-index/order-multiple-entries
另外就是在ENTER_MODULE
中会往queue
中添加一个新的QueueItem
,其action为LEAVE_MODULE
。因为是栈
特性,提前将当前模块的该action
添加到queue
中。而该模块的子modules
则在这新创建的queueItem
后面。然后由于栈特性,会执行完子modules
然后再执行该queueItem
。注意,子blocks
的处理顺序是被延后了的。
queue.push({action: LEAVE_MODULE, block, module, chunk, chunkGroup});
const blockInfo = blockInfoMap.get(block);
const skipBuffer = [];
const queueBuffer = [];
// 遍历同步依赖模块
for (const refModule of blockInfo.modules) {
// 如果当前chunk已经包含过该module,则跳过
if (chunk.containsModule(refModule)) {
continue;
}
// 如果最小可用模块集合包含该模块,说明该模块已经被父chunk加载过
// 则考虑跳过(即当前chunk不需要再重复包含该模块)
if (minAvailableModules.has(refModule)) {
// 注意:action是 ADD_AND_ENTER_MODULE
skipBuffer.push(/*.QueueItem..*/);
continue;
}
// 注意:action是 ADD_AND_ENTER_MODULE
queueBuffer.push(/*.QueueItem..*/);
}
for (let i = skipBuffer.length - 1; i >= 0; i--) {
skippedItems.push(skipBuffer[i]);
}
for (let i = queueBuffer.length - 1; i >= 0; i--) {
queue.push(queueBuffer[i]);
}
// 遍历异步依赖block
for (const block of blockInfo.blocks) iteratorBlock(block);
if (blockInfo.blocks.length > 0 && module !== block) {
blocksWithNestedBlocks.add(block);
}
这一步的主要工作有两点
blockInfo.modules
)构造QueueItem
添加到queue
中,让依赖modules
有机会进入主流程。 请思考:为什么要逆序呢❓ - 栈特性block
(即blockInfo.blocks
)调用iteratorBlock
方法创建新的chunkGroup
/chunk
、维护chunkGroup
的父子关系到queueConnect
变量中、针对该block
创建QueueItem
添加到queueDelayed
(对于异步依赖延迟处理,优先处理同步依赖)下面看下iteratorBlock
方法
const iteratorBlock = b => {
// 1. blockChunkGroups缓存已经创建过chunkGroup的block
// 如果该block已经创建过,则不重复创建
let c = blockChunkGroups.get(b);
if (c === undefined) { // 如果该block没有创建过chunkGroup,则创建
//...
c = compilation.addChunkInGroup(/*..*/);
blockConnections.set(b, []);
} else { //...
// 2. 存储块block连接关系,以便稍后在需要时连接它
blockConnections.get(b).push({
originChunkGroupInfo: chunkGroupInfo,
chunkGroup: c
});
// 3.queueConnect用来临时记录这期间发生的 chunkGroup的父子关系
// key是父,value是子chunkGroup集合
let connectList = queueConnect.get(chunkGroup);
//...
connectList.add(c);
// 4.为该block创建QueueItem并添加到queueDelayed变量中
// 延迟处理,所以叫xxxDelayed
queueDelayed.push({
action: PROCESS_BLOCK, // 注意
block: b, // 注意
module: module,
chunk: c.chunks[0],
chunkGroup: c
});
};
四个部分
compilation.addChunkInGroup
为当前block
创建对应chunkGroup
和chunk
blockConnections
中存储block
连接的信息queueConnect
中记录该期间(内层第一个while)建立的chunkGroup
父子关系,父子关系可以用来计算某个子chunkGroup
的minAvailableModules
即最小可复用模块集合,因为浏览器(假设我们的构架目标是浏览器)总是先加载chunkGroup
中的模块,而后再加载子chunkGroup
中的模块,因此对于父chunkGroup
中已经加载的模块子chunkGroup
可以直接复用,而不必重新加载。因此记录这层关系后就能够计算最小可以复用的模块集合(实际上就是多个父chunkGroup
的模块的交集)。block
创建QueueItem
并添加到queueDelayed
中。异步依赖创建的QueueItem
延迟处理,优先处理同步依赖。
这个过程会把初始化进queue中的模块(实际上就是options.entry指向的模块)的所有同步依赖全部处理完毕。比如在这里main.js
的同步依赖a.js
,以及a.js
的依赖c.js
都会与初始的chunk(Chunk(name = 'chunkMain'))建立连接,而main.js
异步引入的b.js
的情况在上面iteratorBlock
方法中可能会创建了新的chunkGroup
和chunk
,其中新的chunk的name就是代码中注释中提供的chunkB
(import(/* webpackChunkName: "ChunkB",....
),此时新的chunk并未和任何模块建立联系。下面是当前chunkGroup-chunk-modules的关系。
后面的逻辑主要是处理异步依赖的场景。
while (queue.length) {
//...
// 3. 处理 minAvailableModules 收缩的场景
while (queueConnect.size > 0) {
// 3.1 收集该chunkGroup可以复用的模块集合 resultingAvailableModules
for (const [chunkGroup, targets] of queueConnect) {
// 1. chunkGroup和targets是父子关系,收集父chunkGroup中的模块集合 记为 resultingAvailableModules
for (const target of targets) {
// 1. 给target(ChunkGroup类型)创建chunkGroupInfo并保存到chunkGroupInfoMap
// 2. 将上述收集的resultingAvailableModules保存到chunkGroupInfo.availableModulesToBeMerged
// 3. 将chunkGroupInfo添加到 outdatedChunkGroupInfo
}
}
if (outdatedChunkGroupInfo.size > 0) {
// 3.2 计算 chunkGroup的 minAvailableModules,
// 并 重新判断skippedItems和子chunkGroup
for (const info of outdatedChunkGroupInfo) {
let changed = false; // 关键:minAvailableModules 是否发生了变化
// 1. 计算两个集合的交集(做了空间优化-集合对象复用,所以看起来很复杂)
for (const availableModules of availableModulesToBeMerged) {
//...
}
if (!changed) continue;
// 2. 如果minAvailableModules(最小可复用模块)发生变化,
// 则重新考虑之前跳过的模块(即info.skippedItems)
// 3. 当前chunkGroup的minAvailableModules发送了改变,
// 显然其子ChunkGroup的minAvailableModules需要被重新计算
// 因此将这层父子关系添加到queueConnect,
// 利用`while (queueConnect.size > 0) { ... } `进行重新计算
}
}
}
//...
}
这里代码结构上分为两个部分,主要代码量是用来计算多个集合的交集
先要收集多个集合这是第一个for
循环做的事情;第二个for
循环做的事情根据前面收集的多个集合计算交集
。 计算完交集后,如果交集(minAvailableModules
)缩小了,需要重新考虑skippedItems
和子chunkGroup
。
详细步骤和案例如下
收集当前chunkGroup
可以复用的模块集合resultingAvailableModules
:遍历chunkGroup
中的所有chunks
,再遍历chunk
中的所有模块收集为集合。一个子chunkGroup
可能会有多个父chunkGroup
,比如下面例子(两个入口生成了两个EntryPoint(options.name = 'a'/'b'),异步引入的c.js
会创建一个chunkGroup(options.name = 'c')),因此一个子chunkGroup
可以复用的模块有多个集合分别来自不同的父chunkGroup
。
// a.js
import b from './e'
import b from './f'
import(/* webpackChunkName: "c" */ './c')
// b.js
import b from './f'
import b from './g'
import(/* webpackChunkName: "c" */ './c')
// c.js
export let c = 'c';
// webpack.config.js
entry: { a:'./a.js', b: 'b.js' }
计算chunkGrou
p的minAvailableModules
,并重新考虑skippedItems
和子chunkGroup
:上面收集完子chunkGroup
可以复用的模块的集合(可能有多个),这里有一大端代码用来计算集合的交集
。需要注意的是,这里进行了优化,计算的过程是一个集合一个集合的叠加计算交集的。当遍历第一个集合的时候,显然交集就是这个交集就是这个集合,这里直接复用这个集合对象通过minAvailableModulesOwned=false
来表示交集是直接复用的(即没有单独创建一个新的集合对象);当遍历到第二个集合时,会创建一个新集合对象用来存储交集(此时minAvailableModulesOwned=true
,表示交集对象是新创建的没有复用);当遍历到更多的集合时,直接在上面新创建的集合对象上进行计算。至于交集本身的计算逻辑有兴趣的同学可以深入研究一下,尤其是初次创建集合对象时候,实现思路很巧妙(提示:迭代器特性)。
let cachedMinAvailableModules = info.minAvailableModules; // 默认值是undefined
const availableModulesToBeMerged = info.availableModulesToBeMerged; // 集合数组
//...
for (const availableModules of availableModulesToBeMerged) {
if (cachedMinAvailableModules === undefined) { // 第一集合的时候
//...
info.minAvailableModulesOwned = false;
} else {
if (info.minAvailableModulesOwned) { // 第三个集合的时候
//...
} else { // 第二个集合的时候
//...
const newSet = new Set();
//...
info.minAvailableModulesOwned = true;
}
}
//...
}
changed
变量表示此次计算的集合是否发生了变化,如果发生了变化即minAvailableModules
缩小了,skippedItems
和children
需要被重新处理,解释如下:
那么曾经跳过的没有处理的模块(skippedItems
)需要重新添加到queue
中被重新处理(判断是否需要建立链接等逻辑)
// 2. Reconsider skipped items
for (const queueItem of info.skippedItems) {
queue.push(queueItem);
}
info.skippedItems.length = 0;
另外如果当前chunkGroup
的minAvailableModules
发生了变化,那该chunkGroup
的子chunkGroup(info.children)
同样会受到影响,因此将子chunkGroup
需要递归处理(添加到queueConnect
,利用while (queueConnect.size > 0)
进入计算)。
while (queueConnect.size > 0) {
//...
// 3. Reconsider children chunk groups
if (info.children !== undefined) {
const chunkGroup = info.chunkGroup;
for (const c of info.children) {
let connectList = queueConnect.get(chunkGroup);
if (connectList === undefined) {
connectList = new Set();
queueConnect.set(chunkGroup, connectList);
}
connectList.add(c);
}
}
//...
}
可以参考下面案例来理解。
由于异步引用而创建的chunk中的js是可以直接复用父chunk中的模块的,因为父chunk先加载,子chunk后加载,由于父chunk可能存在多个,需要计算出最小可复用模块(minAvailableModules)。
如果minAvailableModules发生了变化,需要考虑带来的影响(skippedItems,children)
while (queue.length) {
while (queue.length) {/*...*/}
//...
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
}
处理上面iteratorBlock
方法创建的QueueItem
(临时存储到queueDelayed
),这里把queueDelayed
赋值给queue
,进入下一轮外层的循环。
这里需要注意的是queueDelayed
中的queueItem.action
的是PROCESS_BLOCK
(跳过了ADD_AND_ENTER_MODULE
和ENTER_MODULE
两个步骤),并且queueItem.block
和queueItem
不是同一个对象,queueItem.block
是AsyncDependenciesBlock
类型,而queueItem.module
是Module
类型。 这里面的queueItem
只会经历PROCESS_BLOCK
每个QueueItem
经历的流程如下:
本文案例执行完buildChunkGraph
的状态如下(得到了初步的chunk graph):