前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >8. 从dependency graph 到 chunk graph

8. 从dependency graph 到 chunk graph

作者头像
tinyant
发布2022-11-23 15:28:10
6600
发布2022-11-23 15:28:10
举报

通过一个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中

聚合的过程可以分为两类:

  1. seal中通过buildChunkGraph中根据内置原则进行初步的优化得到初始chunks
  2. 提供一个钩子 hooks.optimizeChunksBasic ,可以通过订阅这个钩子在初始chunks基础上进一步优化,典型的是webpack内置的SplitChunksPlugin可以根据多种配置来优化chunks(当然这个过程不是必备的)

compilation.seal除了包含聚合的逻辑,还有生成最终内容(构建产物的内容)的逻辑。

compilation.seal方法中的包含大量的钩子,可以参考compilation.seal 中涉及的钩子和函数调用 ,捋完之后实际内置有订阅的插件并不多,遇事不要慌慢慢捋,😄。

在正式进入下面分析之前,先回顾下之前(hooks.make)的成果

可以认为一个dependency graph描述了从初始Dependency(如这里的SingleEntryDependency)开始直到所有依赖的模块的关系,具有request的XxxDependency说明关联一个资源因此会被构建出一个模块,最终该dependency和该模块会被关联起来。

后面初步聚合的工作的主要依据就是上面的dependecy graph.

初步聚合: dependency graph -> chunk graph

总共三个模块,这里的入口只有一个,即webpack.config.js中配置的entry: src/simple/main.js。

代码语言:javascript
复制
// 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进行连接,如ChunkGroupChunkChunkModule的关系就是容器与容器item,连接的好处是可以通过容器找到其包含的所有items,也可以通过item查找其归属的容器。

代码语言:javascript
复制
entry: {
    chunkMain: './src/simple/main.js',
},

上面创建的ChunkEntryPoint都有一个name,等于上面的chunkMain。后面我们会通过Chunk(name=xxx)ChunkGroup(options.name=xxx)这种形式来区分不同的Chunk和ChunkGroup对象

EntryPoint相比ChunkGroup,提供了处理runtimeChunk的能力,后面在代码生成阶段会看到这部分能力。

代码语言:javascript
复制
class Entrypoint extends ChunkGroup {
   // constructor   
   isInitial() // 返回true,表示是初始chunkGroup
   setRuntimeChunk(chunk) // 设置运行时代码应该跟随哪一个chunk
   getRuntimeChunk() {..} //  
   replaceChunk(oldChunk, newChunk) {..} // 替换runtimeChunk
}

所以看到上面for循环中有给EntryPoint设置runtimeChunk属性,因为每个EntryPoint最终输出的文件中需要包含运行时(可以认为是内置模块化机制-兼容各种模块化规范),后面代码输出阶段可以看到runtimeChunk的作用。

下面深入看下buildChunkGraph

buildChunkGraph

代码语言:javascript
复制
// 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);
}

步骤如下:

  1. 分析chunk应该包含的模块(可能会创建新的chunk,chunkgroup)
  2. 链接父子chunkGroup,建立父子关系 => chunk graph
  3. 清理:对于脱离了chunk graph的节点(chunkGroup)被清理掉。

这里重点介绍step1即visitModules

visitModules

初步认识该方法做了什么

这个函数的作用是初步确定最终会有哪些Chunk,每个Chunk分别包含哪些Module;在具体分析这段逻辑之前,先通过一个小案例来直观的感受下这段代码作用后的结果

buildChunkGraph之前的for循环中创建了EntryPoint(也是一个ChunkGroup),EntryPoint关联了一个Chunk,这个Chunk关联了一个entryModule。那么这个Chunk最终需要包含的所有Module都是由这个entryModule及其依赖链上的所有模块组成的,其依赖链上存在同步模块也存在异步模块,对于异步模块是可以拆分成一个单独ChunkGroup

感性认识一下这部分做的事情的例子:

  1. 默认的EntryPoint是上面for循环得到的,右侧ChunkGroup是新生成的(由异步引入import('xx')特性引发,即异步引入一个模块时会创建一个新ChunkGroup
  2. EntryPoint包含一个Chunk,这个Chunk包含了entryModule依赖链上的所有同步模块
  3. 拆分一个新的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+行。核心逻辑本来是可以按照递归的方式写的,但是由于递归可能会有栈溢出问题,所以作者改为了非递归的形式出现,因此大量的变量在这里声明来辅助非递归实现,此外部分关键变量(如queueblockInfoMap)需要完成初始化操作;由于递归本身构成了函数栈,因此改写成非递归形式后需要借助数据结构来模拟函数栈的效果,此外是少不了循环的。这里改写成了栈(对应变量queue,实际是栈的功能push/pop) + while的形式;

// while()前面的注释 // Iterative traversal of the Module graph // Recursive would be simpler to write but could result in Stack Overflows

该方法的核心逻辑如下

代码语言:javascript
复制
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) {/*...*/}
    }
}
  1. 变量声明和初始化
  2. 处理依赖链上的同步依赖模块,延迟处理异步依赖block
  3. 处理 minAvailableModules 收缩的场景
  4. 处理异步依赖 block

总共三个while,外层while,内层第一个while,内层第二个while,下面通过此类描述来指向相关代码片段。

变量声明和初始化

两个重要的变量初始化

blockInfoMap
代码语言:javascript
复制
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);
}

blockInfoMapkeyDependenciesBlock类型,保存每个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')

queue
代码语言:javascript
复制
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

其他在循环过程中用到的变量
代码语言:javascript
复制
let module, chunk, chunkGroup, chunkGroupInfo, block, minAvailableModules, skippedItems;

后面的while会先queue.pop()弹出QueueItem从中取出相应属性赋值给变量module, block, chunk, chunkGroup ,从chunkGroupInfoMap获取chunkGroupInfo,然后拿到属性minAvailableModules, skippedItems并赋值给上面的变量。

声明为函数作用域变量的好处:

  1. 方便直接获取,否则每次还得读取属性
  2. 整个函数作用域共享,内部的函数调用时就不需要将参数传来传去

处理依赖链上的同步依赖模块,延迟处理异步依赖block

重点关注下面第二个部分

代码语言:javascript
复制
//...

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

下面我们具体看下流程中的每个节点都做了些什么操作。

ADD_AND_ENTER_MODULE
代码语言:javascript
复制
if (minAvailableModules.has(module)) {
    // 父chunk已经包含过,这里先在跳过它,
    // 但是当 minAvailableModules 缩小时会重新检查(后面会给出具体的案例)
    skippedItems.push(queueItem);
    break;
}

// 建立chunk和module的连接
if (chunk.addModule(module)) {
    module.addChunk(chunk);
} else { // 如果已经包含了,则跳过
    break;
}
  1. 如果当前 minAvailableModules 包含过该模块,则暂时先跳过即保存到skippedItems,如果minAvailableModules缩小了,则skippedItems中这些曾经跳过的模块需要重新跑流程。
  2. 如果当前chunk没有包含过该模块,则chunk和module相互建立连接,否则break即进入下一个模块的流程。
ENTER_MODULE & LEAVE_MODULE

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的处理顺序是被延后了的。

代码语言:javascript
复制
queue.push({action: LEAVE_MODULE, block, module, chunk, chunkGroup});
PROCESS_BLOCK
代码语言:javascript
复制
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 &amp;&amp; module !== block) {
    blocksWithNestedBlocks.add(block);
}

这一步的主要工作有两点

  1. 对同步依赖模块(即blockInfo.modules)构造QueueItem添加到queue中,让依赖modules有机会进入主流程。 请思考:为什么要逆序呢❓ - 栈特性
  2. 对异步依赖block(即blockInfo.blocks)调用iteratorBlock方法创建新的chunkGroup/chunk、维护chunkGroup的父子关系到queueConnect变量中、针对该block创建QueueItem添加到queueDelayed(对于异步依赖延迟处理,优先处理同步依赖)

下面看下iteratorBlock方法

iteratorBlock
代码语言:javascript
复制
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
   });
};

四个部分

  1. 调用compilation.addChunkInGroup为当前block创建对应chunkGroupchunk
  2. blockConnections中存储block连接的信息
  3. queueConnect中记录该期间(内层第一个while)建立的chunkGroup父子关系,父子关系可以用来计算某个子chunkGroupminAvailableModules即最小可复用模块集合,因为浏览器(假设我们的构架目标是浏览器)总是先加载chunkGroup中的模块,而后再加载子chunkGroup中的模块,因此对于父chunkGroup中已经加载的模块子chunkGroup可以直接复用,而不必重新加载。因此记录这层关系后就能够计算最小可以复用的模块集合(实际上就是多个父chunkGroup的模块的交集)。
  4. 为当前block创建QueueItem并添加到queueDelayed中。异步依赖创建的QueueItem延迟处理,优先处理同步依赖。
小结

这个过程会把初始化进queue中的模块(实际上就是options.entry指向的模块)的所有同步依赖全部处理完毕。比如在这里main.js的同步依赖a.js,以及a.js的依赖c.js都会与初始的chunk(Chunk(name = 'chunkMain'))建立连接,而main.js异步引入的b.js的情况在上面iteratorBlock方法中可能会创建了新的chunkGroupchunk,其中新的chunk的name就是代码中注释中提供的chunkBimport(/* webpackChunkName: "ChunkB",....),此时新的chunk并未和任何模块建立联系。下面是当前chunkGroup-chunk-modules的关系。

后面的逻辑主要是处理异步依赖的场景。

计算minAvailableModules,重新考虑skippedItems

代码语言:javascript
复制
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

详细步骤和案例如下

  • step 1

收集当前chunkGroup可以复用的模块集合resultingAvailableModules:遍历chunkGroup中的所有chunks,再遍历chunk中的所有模块收集为集合。一个子chunkGroup可能会有多个父chunkGroup,比如下面例子(两个入口生成了两个EntryPoint(options.name = 'a'/'b'),异步引入的c.js会创建一个chunkGroup(options.name = 'c')),因此一个子chunkGroup可以复用的模块有多个集合分别来自不同的父chunkGroup

代码语言:javascript
复制
// 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' }

计算chunkGroup的minAvailableModules,并重新考虑skippedItems和子chunkGroup:上面收集完子chunkGroup可以复用的模块的集合(可能有多个),这里有一大端代码用来计算集合的交集。需要注意的是,这里进行了优化,计算的过程是一个集合一个集合的叠加计算交集的。当遍历第一个集合的时候,显然交集就是这个交集就是这个集合,这里直接复用这个集合对象通过minAvailableModulesOwned=false来表示交集是直接复用的(即没有单独创建一个新的集合对象);当遍历到第二个集合时,会创建一个新集合对象用来存储交集(此时minAvailableModulesOwned=true,表示交集对象是新创建的没有复用);当遍历到更多的集合时,直接在上面新创建的集合对象上进行计算。至于交集本身的计算逻辑有兴趣的同学可以深入研究一下,尤其是初次创建集合对象时候,实现思路很巧妙(提示:迭代器特性)。

代码语言:javascript
复制
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缩小了,skippedItemschildren需要被重新处理,解释如下:

  • step 2

那么曾经跳过的没有处理的模块(skippedItems)需要重新添加到queue中被重新处理(判断是否需要建立链接等逻辑)

代码语言:javascript
复制
// 2. Reconsider skipped items
for (const queueItem of info.skippedItems) {
   queue.push(queueItem);
}
info.skippedItems.length = 0;
  • step 3

另外如果当前chunkGroupminAvailableModules发生了变化,那该chunkGroup的子chunkGroup(info.children)同样会受到影响,因此将子chunkGroup需要递归处理(添加到queueConnect,利用while (queueConnect.size > 0)进入计算)。

代码语言:javascript
复制
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)

异步依赖block,queueDelayed

代码语言:javascript
复制
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_MODULEENTER_MODULE两个步骤),并且queueItem.blockqueueItem不是同一个对象,queueItem.blockAsyncDependenciesBlock类型,而queueItem.moduleModule类型。 这里面的queueItem只会经历PROCESS_BLOCK

总结

每个QueueItem经历的流程如下:

本文案例执行完buildChunkGraph的状态如下(得到了初步的chunk graph):

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2022-11-03,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 初步聚合: dependency graph -> chunk graph
  • buildChunkGraph
  • visitModules
    • 初步认识该方法做了什么
      • 解释 代码中用到的关键变量
        • 代码逻辑分析
          • 变量声明和初始化
          • 处理依赖链上的同步依赖模块,延迟处理异步依赖block
          • 计算minAvailableModules,重新考虑skippedItems
          • 异步依赖block,queueDelayed
      • 总结
      相关产品与服务
      容器服务
      腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档