zl程序教程

您现在的位置是:首页 >  其他

当前栏目

8-1. 「webpack源码分析」一个具体案例再次深入看buildChunkGraph的运行过程

案例webpack源码 分析 深入 一个 运行 过程
2023-06-13 09:17:12 时间

只有同步依赖block (Module类型)

// 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值:】 []

这里重点关注两个结论:

  • 栈特性:当前模块(如a.js)的同步依赖模块处理完后(如这里的b.js,如果有多个同步一样,需要多个同步依赖都处理完)才会结束当前模块处理流程。
  • 对于entryModule,流程起点从ENTER_MODULE开始,而对于依赖模块如上面的b.js是从ADD_AND_ENTER_MODULE。因为入口模块已经和当前chunk建立过联系了(见compilation.seal方法,调用buildChunkGraph之前的for循环里)

存在异步依赖block(AsyncDependenciesBlock类型)

重点想体现可用模块收缩的场景。

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)
}))

外层while第一次循环

补充

  • blockInfoMap
  • 初始queue

内层循环第一个while

步骤

解释

初始

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) {
        //...
    }
}

内层循环第二个while

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,后面循环还会碰到,这里先跳过

queueDelayed赋值给queue

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;
}

外层while第二次循环

此时的queue就是上面的queueDelayed

内层循环第一个while

步骤

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 = []

内层循环第二个while

此时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

外层while第三次循环

内层循环第一个while

  • f.js ,由于chunkGroup(options.name = C)收缩后minAvailableModules不再包含f.js,因此会建立chunk和module联系
  • g.js 收缩后minAvailableModules依然包含g.js,因此依然跳过push到skippedItems

内层循环第二个while

queueConnect.size = 0(上面的循环没有发生新的异步引用)不再进入

queueDelayed赋值给queue

queueDelayed = [{"action":"PROCESS_BLOCK","request":"./C","syncBlock":false}]

外层while第四次循环

内层循环第一个while

C.js(其queueItem.block是AsyncDependenciesBlock类) 直接进入PROCESS_BLOCK,由于C.js已经和当前chunk(name = C)建立过联系,因此不再处理。

内层循环第二个while(结束)

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

  • EnsureChunkConditionsPlugin 作用 ❓
  • RemoveParentModulesPlugin 作用 ❓