// a.js
import b from './b.js';
// b.js
export const b = 'b';
// webpack.config.js
entry: {a: 'a.js'}
这部分逻辑主要是下面内循环的第一个while
循环。
while (queue.length) {
while (queue.length) { // 这里
switch (queueItem.action) {
// ADD_AND_ENTER_MODULE、ENTER_MODULE、PROCESS_BLOCK、LEAVE_MODULE
}
}
//...
}
queue的初始化及其含义参考上面的变量解释的表格,当前demo只有一个入口即a.js,因此此时queue只有一个元素,module就是'a.js'(entryModule),action是ENTER_MODULE
,由于entryModule和其所在的Chunk已经建立过关系,因此跳过ADD_AND_ENTER_MODULE
节点,直接来到ENTER_MODULE
看下这里的完整执行流程,如下
内部的while(queue.length)一共走了四次,
初始queue: [{ module: a.js, action: ENTER_MODULE }]
内循环 | 执行过程 |
---|---|
第一次(a.js) | [ENTER_MODULE]: 先是从queue中弹出entryModule进入ENTER_MODULE节点,该节点主要push一个新的QueueItem,module依然是当前模块即entryModule,但是action为LEAVE_MODULE,因为当该模块的所有同步依赖block都处理完成后,需要执行leave逻辑;[PROCESS_BLOCK]: 执行完ENTER_MODULEL逻辑会直接进入(源码中叫fallthrough,即当前case不会break)到PROCESS_BLOCK节点,这一步主要是收集依赖的block(同步和异步),当前demo只有同步依赖block即NormalModule(rawRequest='./b.js'),对于同步依赖block(实际就是模块)会构造QueueItem然后push到queue中,其action是ADD_AND_ENTER_MODULE,然后会在当前节点退出(case-break)进入下一次循环 ------------------------------------------------------------ 【执行结束后的queue值:】 [ { module: a.js, action: LEAVE_MODULE }, { module: b.js, action: ADD_AND_ENTER_MODULE } ] |
第二次(b.js) | [ADD_AND_ENTER_MODULE]:弹出模块b.js,进入ADD_AND_ENTER_MODULE节点,判断当前chunk和该模块是否建立过联系,建立过则直接结束当前模块处理,进入下一次循环;如果没建立则chunk和module相互建立连接;[ENTER_MODULE]:然后进入到ENTER_MODULE逻辑,后面的处理和之前的entryModule类似,不再赘述。 ------------------------------------------------------------ 【执行结束后的queue值:】 [ { module: a.js, action: LEAVE_MODULE }, { module: b.js, action: LEAVE_MODULE } ] |
第三次(b.js) | 弹出模块b.js,进入LEAVE_MODULE,当前只是设置index2,作用是啥❓❓❓ ------------------------------------------------------------ 【执行结束后的queue值:】 [ { module: a.js, action: LEAVE_MODULE } ] |
第四次(a.js) | 弹出模块a.js,进入LEAVE_MODULE,同上。------------------------------------------------------------ 【执行结束后的queue值:】 [] |
这里重点关注两个结论:
同步依赖模块
处理完后(如这里的b.js,如果有多个同步一样,需要多个同步依赖都处理完)才会结束当前模块处理流程。
ENTER_MODULE
开始,而对于依赖模块如上面的b.js是从ADD_AND_ENTER_MODULE
。因为入口模块已经和当前chunk建立过联系了(见compilation.seal方法,调用buildChunkGraph之前的for循环里)
重点想体现可用模块收缩的场景。
var actionMap = ['ADD_AND_ENTER_MODULE','ENTER_MODULE','PROCESS_BLOCK','LEAVE_MODULE']
//
queue.map(item=>({action: actionMap[item.action],request: (item.module === item.block) ? item.module.rawRequest: item.block.request, syncBlock: item.module === item.block}))
//
queueDelayed.map(item=>({action: actionMap[item.action], request: item.block.request , syncBlock: item.module === item.block}))
// 获取
[...queueConnect.entries()].map(item => ({
parentChunkGroup: item[0].options.name,
childrenChunkGroups: [...item[1]].map(item => item.options.name)
}))
补充
步骤 | 解释 |
---|---|
初始 | |
A1.js进入循环 | ENTER_MODULE -> PROCESS_BLOCK,这里有同步依赖模块也有异步依赖block,看下处理的异同: 【相同点:】 同步依赖模块同样是构造QueueItem并push到queue中, 【差异点:】 而对·异步依赖block(AsyncDependenciesBlock)会调用iteratorBlock方法创建出新的ChunkGroup和Chunk,构造QueueItem并push到queueDelayed中,注意该Item的module和block是不相等的,另外会通过queueConnection记录父子ChunkGroup的关系 Map<ChunkGroup, Set<ChunkGroup>> 【queue值】syncBlock为true,表示当前block是一个Module,否则是AsyncDepnedencyBlock 所以这里有三个同步依赖模块,和两个异步依赖block,和A1.js的内容是对应上的。 |
g、f、e分别进入循环 | 模块g、f、e分别进入三两次循环完成(ADD_AND_ENTER_MODULE、LEAVE_MODULE),因为这三个模块自身没有其他依赖了,所以这三个模块至此结束,并添加到A1.js所在的Chunk中 【执行过程】 ADD_AND_ENTER_MODULE ./e => LEAVE_MODULE ./e => ADD_AND_ENTER_MODULE ./f => LEAVE_MODULE ./f => ADD_AND_ENTER_MODULE ./g => LEAVE_MODULE ./g |
A1.js | LEAVE_MODULE 【queue值】 [{action: 'ENTER_MODULE', request: './src/demo5/A2.js', syncBlock: true}] |
A2.js进入循环 | 同A1.js逻辑一致 【关键变量的值如下】 :,背景色部分是此次新添加的,由于Chunk(name = 'B')之前已经创建过,这里不会再次创建, |
A2.js进入循环 | LEAVE_MODULE 【queue值】 [] |
至此,入口模块的同步依赖模块都已经处理完成,上述流程是在内部的第一个while
完成的
while (queue.length) {
while (queue.length) { // 这里
switch (queueItem.action) {
//...
}
}
}
但是异步依赖block尚未处理,目前都存储在queueDelayed中,当queue为空后,会进入下面的 while (queueConnect.size > 0)
逻辑,该while
内部的主要作用计算
我们先简单捋下这里的逻辑,再回过头看实际的运行过程
while (queue.length) {
//...
while (queueConnect.size > 0) {
for (const [chunkGroup, targets] of queueConnect) {
// 收集该chunkGroup可以复用的模块集合 resultingAvailableModules
for (const target of targets) {
// 1. 给target(ChunkGroup类型)创建chunkGroupInfo并保存到chunkGroupInfoMap
// 2. 将上述收集的resultingAvailableModules保存到chunkGroupInfo.availableModulesToBeMerged
// 3. 将chunkGroupInfo添加到outdatedChunkGroupInfo
}
}
if (outdatedChunkGroupInfo.size > 0) {
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) { ... } `进行重新计算,一个字秒!!!
}
//...
}
}
if (queue.length === 0) {
//...
}
}
queueConnect如下
[
{
childrenChunkGroups: ['B', 'C'],
parentChunkGroup: 'chunkA1',
},
{
childrenChunkGroups: ['B'],
parentChunkGroup: 'chunkA2',
},
];
[...outdatedChunkGroupInfo].map(item => ({
chunkGroupName: item.chunkGroup.options.name,
availableModulesToBeMerged: item.availableModulesToBeMerged.map(item => ([...item].map(item => item.rawRequest)))
}))
[...chunkGroupInfoMap.entries()].map(item => ({
chunkGroupName: item[1].chunkGroup.options.name,
minAvailableModules: [...item[1].minAvailableModules].map(item => item.rawRequest)
}))
过程 | 解释 |
---|---|
1. 遍历queueConnect(保存着异步引用的父子关系)得到outdatedChunkGroupInfo | 分别为两个ChunkGroup(options.name = B、C)创建chunkGroupInfo;收集父ChunkGroup(options.name = chunkA1)上所有chunks上的所有module(即resultingAvailableModules):["./src/demo5/A1.js", "./e", "./f", "./g"]记为moduleSetA1,收集父ChunkGroup(options.name = chunkA2)的所有模块["./src/demo5/A2.js"]记为moduleSetA2 A1.js和A2.js都异步引用了B.js,C.js只被A1.js异步引用,因此B.js在运行时候实际上有可能访问到这[moduleSetA1, moduleSetA2],C.js只可能访问[moduleSetA1] 【outdatedChunkGroupInfo中的部分信息如下:】 |
2. 遍历 outdatedChunkGroupInfo计算每个chunkGroupInfo的minAvailableModules | 计算两个集合的交集。 此时这两个新的chunkGroupInfo都还没有添加skippedItems和children,这部分逻辑暂时略过 【为什么要计算交集】: 请读者思考❓❓❓ |
3. 处理chunkGroupInfo.skippedItems/children | 新创建的两个chunkGroupInfo暂时没有skippedItems/children,后面循环还会碰到,这里先跳过 |
queueDealyed的部分信息如下,queueItem.block都是ImportDependenciesBlock
,只有一个依赖(Dependency)ImportDependency
[{action: 'PROCESS_BLOCK', request: './B', syncBlock: false},
{action: 'PROCESS_BLOCK', request: './C', syncBlock: false},
{action: 'PROCESS_BLOCK', request: './B', syncBlock: false}]
if (queue.length === 0) {
const tempQueue = queue;
queue = queueDelayed.reverse();
queueDelayed = tempQueue;
}
此时的queue就是上面的queueDelayed
步骤 | |
---|---|
B.js(其queueItem.block是AsyncDependenciesBlock类型) | [PROCESS_BLOCK]: 此时只有一个同步依赖模块(rawRequest = "./B"),构造QueueItem(其中action为ADD_AND_ENTER_MODULE)并push到queue中;注意当前block不会执行其他三个节点(ADD_AND_ENTER_MODULE等) |
B.js(其queueItem.block是Module类型) | 建立chunk(name = "B")和该module联系,进入ENTER_MODULE和PROCESS_BLOCK。 在PROCESS_BLOCK中的逻辑:【同步依赖模块】 这里有g、h、i三个同步依赖模块,分别创建QueueItem添加到queue中 【异步依赖block -> iterateBlock】1. 添加新的QueueItem到queueDelayed中 2. 创建一个新的chunkGroup(options.name = "C"); ,3. 通过queueConnect记录ChunkGroup的父子关系。 |
i、h、g | 分别ADD_AND_ENTER_MODULE、ENTER_MODULE、LEAVE_MODULE 当前chunkGroupInfo.minAvailableModules是空,因此这三个模块都会被添加到当前chunkGroup(options.name = 'B'); |
B.js | LEAVE_MODULE , queue = [{"action":"PROCESS_BLOCK","request":"./B","syncBlock":false},{"action":"PROCESS_BLOCK","request":"./C","syncBlock":false}] |
C.js(其queueItem.block是AsyncDependenciesBlock类型) | [PROCESS_BLOCK]: 只有一个同步依赖模块(rawRequest = "./C"),创建QueueItem(其中action为ADD_AND_ENTER_MODULE)添加到queue中 |
C.js(其queueItem.block是Module类型 | 建立chunk(name = "C")和该module联系,进入ENTER_MODULE和PROCESS_BLOCK。 在PROCESS_BLOCK中的逻辑:【同步依赖模块】 这里有f、g、j三个同步依赖模块,分别创建QueueItem添加到queue中 没有 blocks因此queueDelayed和queueConnect没有变化 ; 当前chunkGroup(options.name = C)的minAvailableModules是 ["./src/demo5/A1.js", "./e", "./f", "./g"];因此对于模块f、g创建QueueItem不会添加到queue中而是添加到skippedItems(临时跳过而已)中;会给模块j创建QueueItem添加到queue中 queue = [{"action": "PROCESS_BLOCK", "request": "./B", "syncBlock": false},{"action": "LEAVE_MODULE", "request": "./C", "syncBlock": true},{"action": "ADD_AND_ENTER_MODULE", "request": "./j", "syncBlock": true}] |
j.js | 【ADD_AND_ENTER_MODULE】 完成chunkGroup和模块的连接,随后ENTER_MODULE -> PROCESS_BLOCK ,而后下一次循环LEAVE_MODULE |
B.js(其queueItem.block是AsyncDependenciesBlock类型) | [PROCESS_BLOCK]: 当前chunk已经包含过该模块(rawRequest = './B'),因此不会再创建QueueItem处理; 此时 queue = [] |
此时queueConnect和queueDelayed的值如下(是由B.js异步引用C.js生成的信息)
// queueDelayed = [{"action":"PROCESS_BLOCK","request":"./C","syncBlock":false}]
// queueConnect = [{"parentChunkGroup":"B","childrenChunkGroups":["C"]}]
阶段 | |
---|---|
1. 遍历queueConnect(保存着异步引用的父子关系)得到outdatedChunkGroupInfo | [{"chunkGroupName":"C","availableModulesToBeMerged":[["./B","./g","./h","./i"]]}] |
2. 遍历 outdatedChunkGroupInfo计算每个chunkGroupInfo的minAvailableModules | 再次之前chunkGroup(options.name = C)的chunkGroupInfo.minAvailableModules = ["./src/demo5/A1.js", "./e", "./f", "./g"] ,经过交集计算后minAvailableModules收缩为["./g"] |
3. 处理chunkGroupInfo.skippedItems/children | 由于minAvailableModules发生了变化,之前跳过的模块此时可能不应该再被跳过了,因此会将skippedItems赋值给queue重新跑一次。 chunkGroup(options.name = C)的skippedItems分别是g.js和f.js对应的QueueItem |
queueConnect.size = 0(上面的循环没有发生新的异步引用)不再进入
queueDelayed = [{"action":"PROCESS_BLOCK","request":"./C","syncBlock":false}]
C.js(其queueItem.block是AsyncDependenciesBlock
类) 直接进入PROCESS_BLOCK,由于C.js已经和当前chunk(name = C)建立过联系,因此不再处理。
queue和queueDelayed均为空,结束循环,退出visitModules
skippedItems 表示上面计算依赖链的时候跳过的模块,但是因为minAvailableModules会发生变化,有些模块就不应该被跳过了 ,如果 minAvailableModules 变化(因为是求交集,只可能减少),则需要重新计算skippedItems ,比如之前 skippedItems: [a,b,c] , minAvailableModules: [a,b,c,d,e],现在minAvailableModules变为了:[a, b] (说明新的异步引用的父chunkGroup没有使用c,d,e模块) ,skippedItems 加入到queue中,重新计算是否需要跳过,如果可以跳过则继续跳过,比如这里的a,b,如果不能跳过,说明子chunkGroup.chunk需要依赖他即需要建立链接-ADD_AND_ENTER_MODULE(没有任何父chunkGroup提供该模块用来共享) ,当前这个阶段,chunkGroup只会包含一个chunk中,发生在compilation.seal和上面的 iteratorBlock ,buildChunkGraph阶段完成后,在compilation.seal 的后面逻辑会有很多chunks相关的钩子已进行优化,此时有可能构成一对多的关系。
// 如果当前info.chunkGroup.minAvailableModules变化了(上面的changed=true) // 那么显然 info.chunkGroup关联的子chunkGroup的minAvailableModules也需要重新计算 // 这是一个递归的过程 // 可以想象一个场景,一颗多叉树中的每个节点携带一个value, sum字段,value是节点自身权重 // sum是父节点的sum加上当前节点的value,如果一个父节点的value值发生了变化,那是不是得递归遍历 // 这个父节点的所有孩子节点,并更新sum值 (大致是这个意思,父节点的变更会影响其孩子节点,然后是递归的)
chunkGroup父子关系的建立,会给chunk的优化提供支撑,如 EnsureChunkConditionsPlugin.js、RemoveParentModulesPlugin.js 就用到chunkGroup.parentsIterable