关于webpack:Webpack5源码seal阶段流程图分析一

58次阅读

共计 47822 个字符,预计需要花费 120 分钟才能阅读完成。

本文内容基于 webpack 5.74.0 版本进行剖析

前言

  1. 因为 webpack5 整体代码过于简单,为了缩小复杂度,本文所有剖析将只基于 js 文件类型进行剖析,不会对其它类型(cssimage)进行剖析,所举的例子也都是基于 js 类型
  2. 为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码
  3. 文章默认读者曾经把握 tapableloaderplugin 等基础知识,对文章中呈现 asyncQueuetapableloaderplugin 相干代码都会间接展现,不会减少过多阐明
  4. 因为 webpack5 整体代码过于简单,因而会抽离出外围代码进行剖析解说

外围代码是笔者认为外围代码的局部,必定会造成局部内容(读者也感觉是外围代码)缺失,如果发现缺失局部,请参考其它文章或者私信 / 评论区告知我

文章内容

编译入口 ->make->seal,而后进行seal 阶段整体流程的概述(以流程图和简化代码的模式),而后依据流程图抽离进去的外围模块开展具体的剖析,在剖析过程中,会着重剖析:

  • ModuleChunkChunkGroupChunkGraph之间的关系
  • 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<Entrypoint, Module[]>} */
    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,比方EntryDependencyConcatenatedModule
不同类型的 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 能够是其它 ChunkGroupparent或者 child
EntryPoint 是入口类型的ChunkGroup,蕴含了入口Chunk

下图来自 An in-depth perspective on webpack’s bundling process

ChunkGraph

治理 module、chunk 和 chunkGroup 之间的关系

上面的类图并没有写全属性,只是写上笔者认为重要的属性,上面两个图只是为了更好了解 ChunkGraph 的作用以及治理逻辑,不是作为概括应用

2. 遍历 this.entries,创立 Chunk 和 ChunkGroup

  1. 进行 new ChunkGraph() 的初始化
  2. 遍历 this.entries 汇合,依据 name 进行 addChunk() 创立一个新的Chunk,并且创立对应的new Entrypoint(),也就是ChunkGroup
  3. 进行一系列对象的存储:namedChunkGroupsentrypointschunkGroups,为后续的逻辑做筹备
  4. 最初进行 chunk 和 ChunkGroup 的关联: connectChunkGroupAndChunk()
  5. 最初进行 this.entries.dependencies 的遍历,因为一个入口 Chunk 可能存在多个文件,比方 entry: {A: ["1.js", "2.js"]}ChunkA 存在 1.js2.js,此时的 this.entries.dependencies 就是 1.js2.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() 建设起 ChunkGroupChunk的关联

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.js2.js,此时的 this.entries.dependencies 就是 1.js2.js

  1. 通过 dep 获取对应的 NormalModule,即利用dependency 获取对应的Module 对象
  2. 应用 chunkGraph.connectChunkAndEntryModule() 关联 chunk、module 和 chunkGroup 的关系
  3. 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<Entrypoint, Module[]>} */
    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.js2.js,此时的 this.entries.dependencies 就是 1.js2.jschunkGraphInit依据 entrypoint 创立的数组蕴含 1.js2.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 进行相互关联,实质就是 availableSourcesavailableChildren相互增加对方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,而后进行chunkmodulechunkGroup 的关联

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

chunkmodule进行相互关联

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(),其中会波及到originModuledependencymodule 等变量

// 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.js
  • dependency: 是父 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() 会失去一个依赖数据对象 blockModulesgetBlockModules() 通过以后 module 获取所有的同步依赖,即上面示例中的Array(14)

processBlock()- 解决同步依赖

通过下面的剖析,咱们通过 getBlockModules() 获取以后 block 的所有同步依赖后,咱们对这些依赖进行遍历

同步依赖的 block=module,异步依赖就传递不同的参数,如上面的queueBuffer 的数据结构,blockmodule 都是同一个数据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.jsasyncChunks=false/chunkLoading=false,还是应用目前的Chunk,与同步依赖集成在同一文件中
    • 场景 3: 非 Entry+容许 asyncChunk的状况,应用 addChunkInGroup() 建设新的 ChunkGroup 和新的Chunk,造成新的文件寄存该异步依赖
  • 状况 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 总结

  1. 解决同步的依赖 -> 将异步依赖退出队列中 -> 将异步依赖的异步依赖放入到 Set()中
  2. 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 的可复用的最小模块,能够应用一个示例简略了解可复用的最小模块:

  • 目前 parentModuleentry.js,它有同步依赖a.jsb.jsc.js,异步依赖async_B.js
  • 目前异步依赖 async_B.js 能够造成新的 ChunkChunkGroup,它有同步依赖a.jsb.js
  • 因为异步依赖 async_B.js 的加载工夫必定慢于 parentModule 的同步依赖,因而异步依赖 async_B.js 能够间接复用 parentModule 的同步依赖 a.jsb.js,而不必把a.jsb.js 打包进去本人的Chunk

ChunkGroupInfo.minAvailableModules 就是 a.jsb.jsNormalModule汇合

理分明 minAvailableModules 的概念后,咱们就能够对上面代码进行剖析:

  • 遍历以后 ChunkGroupInfo 的所有 parent ChunkGroupInfo,即info.availableSources,而后计算出它们的resultingAvailableModules 可复用的模块,而后一直合并到以后 ChunkGroupInfoavailableModules属性中
  • 最终进行 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 的同步依赖和异步依赖的解决

如上面代码块所示,c实际上是一个非入口类型的 chunkGroup
queueConnect 存储的是:

  • key: 以后ChunkGroupInfo
  • value: 非入口类型创立的新 chunkGroup 汇合数组
// 解决 NormalModule 的异步依赖 b
const iteratorBlock = b => {
    // 如果 c 之前不存在,须要从新建设,这里只是为了更好了解而摘出这部分代码
    c = compilation.addChunkInGroup(
        b.groupOptions || b.chunkName,
        module,
        b.loc,
        b.request
    );
    c.index = nextChunkGroupIndex++;
    if (c !== undefined) {
        // b 为非入口的异步依赖
        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);
        queueDelayed.push({
            action: PROCESS_BLOCK,
            block: b,
            module: module,
            chunk: c.chunks[0],
            chunkGroup: c,
            chunkGroupInfo: cgi
        });
    } else if (entrypoint !== undefined) {chunkGroupInfo.chunkGroup.addAsyncEntrypoint(entrypoint);
    }
}

4.6.2 解决 queueConnect 数据

iteratorBlock() 中进行 queueConnect 数据的构建后
processQueue()的内层循环完结时,咱们会进行 queueConnect 数据的对立解决

每一次遍历完 queue,都会触发一次 queueConnect.size 的检测

while (queue.length || queueConnect.size) {processQueue();
    if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
    }
    if (queueConnect.size > 0) {
        // calculating available modules
        processConnectQueue();

        if (chunkGroupsForMerging.size > 0) {
            // merging available modules
            processChunkGroupsForMerging();}
    }
    //...
    if (queue.length === 0) {
        const tempQueue = queue;
        queue = queueDelayed.reverse();
        queueDelayed = tempQueue;
    }
}

processConnectQueue()解决以后 ChunkGroupInfo 的异步依赖,此时

  • chunkGroupInfo: 以后的ChunkGroupInfo
  • targets:以后的 ChunkGroupInfo 的异步依赖中非入口类型新建的 ChunkGroup 汇合数组

上面代码整体流程能够概括为:

  • 先将非入口类型异步依赖新建的 ChunkGroup 都退出到以后的 ChunkGroupInfo.children
  • 计算出以后的 ChunkGroupInfo 最小可复用的 module 汇合数据,而后增加到新建的 ChunkGroup.availableModulesToBeMerged 属性中
  • 将非入口类型异步依赖新建的 ChunkGroup 都退出到 chunkGroupsForMerging 汇合中,筹备下一个阶段
const processConnectQueue = () => {// 解决异步依赖创立的 <ChunkGroupInfo, chunkGroup[]> 之间的关联
    for (const [chunkGroupInfo, targets] of queueConnect) {
        // 1. Add new targets to the list of children
        for (const target of targets) {chunkGroupInfo.children.add(target);
                }
        
        // 2. Calculate resulting available modules
        const resultingAvailableModules =
            calculateResultingAvailableModules(chunkGroupInfo);

        const runtime = chunkGroupInfo.runtime;

        // 3. Update chunk group info
        for (const target of targets) {target.availableModulesToBeMerged.push(resultingAvailableModules);
            chunkGroupsForMerging.add(target);
            const oldRuntime = target.runtime;
            const newRuntime = mergeRuntime(oldRuntime, runtime);
            if (oldRuntime !== newRuntime) {
                target.runtime = newRuntime;
                outdatedChunkGroupInfo.add(target);
            }
        }

        statConnectedChunkGroups += targets.size;
    }
    queueConnect.clear();};

4.6.3 解决 chunkGroupsForMerging 数据

在下面调用 processConnectQueue() 解决实现 queueConnect 数据后,会触发 processChunkGroupsForMerging() 解决 chunkGroupsForMergings 数据

while (queue.length || queueConnect.size) {processQueue();
    if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
    }
    if (queueConnect.size > 0) {
        // calculating available modules
        processConnectQueue();

        if (chunkGroupsForMerging.size > 0) {
            // merging available modules
            processChunkGroupsForMerging();}
    }
    //...
    if (queue.length === 0) {
        const tempQueue = queue;
        queue = queueDelayed.reverse();
        queueDelayed = tempQueue;
    }
}

注:因为 processChunkGroupsForMerging() 代码量过多,因而为了简化解决,将应用一个示例解说该办法,并且只保留示例会运行的条件代码

如上图所示,有两个入口会同时持有异步依赖 async_B.js,在下面processConnectQueue() 的剖析中,咱们能够晓得,应用 calculateResultingAvailableModules() 能够计算出 resultingAvailableModules 为:

  • entry1.js['./src/entry1.js', './item/entry1_a.js', './item/entry1_b.js', './item/common_____g.js']
  • entry2.js['./src/entry2.js', './item/entry1_b.js', './item/entry2_aa', './item/common_____g.js']

而后触发 target.availableModulesToBeMerged.push(resultingAvailableModules),会将下面失去的两个数组放入到ChunkGroupInfo.availableModulesToBeMerged 数据中,最终这些数据会带到 processChunkGroupsForMerging()

如上面 processChunkGroupsForMerging() 所示,一开始因为 cachedMinAvailableModules 为空,会先赋值一个 resultingAvailableModulescachedMinAvailableModules,而后再开始比拟计算并集
如上面代码正文所示,计算并集的逻辑其实也不难懂,先拿出 cachedMinAvailableModules[i],而后比对availableModules 有没有蕴含这个数据,如果没有,则阐明得计算并集,最终触发outdatedChunkGroupInfo.add(info),进行下一个阶段的解决

为什么要计算并集其实也很好了解,如咱们下面所剖析那样
entry1.js 能够为 async_B.js 一些复用的 module,entry2.js能够为 async_B.js 一些复用的 module
程序会先加载同步依赖(即复用的 module),再加载 async_B.js
那么如果 async_B.js 外部本人也 import 这些复用的 module 作为同步依赖,那么就不必把这些可复用的 module 打包进去 async_B.js 所造成的 Chunk 了,因为能够间接应用 Parent Chunk 的同步依赖
然而 entry1.jsentry2.js能够提供复用的 module 有一些是不一样的怎么办?
比方 entry1.js 能够提供 a、b、c,entry2.js能够提供 b、c、d、e,async_B.js须要的同步依赖是 a、c
因为不分明是先加载哪个入口文件,因而只能计算 entry1.jsentry2.js提供复用的 module 的并集,也就是 b、c
因而 async_B.js 如果须要 b、c,那就不必额定打包了,间接复用即可,然而理论 async_B.js 须要的同步依赖是 a、c,因而 async_B.js 还得把 a 打包进去

const processChunkGroupsForMerging = () => {for (const info of chunkGroupsForMerging) {
        const availableModulesToBeMerged = info.availableModulesToBeMerged;
        let cachedMinAvailableModules = info.minAvailableModules;

        if (availableModulesToBeMerged.length > 1) {availableModulesToBeMerged.sort(bySetSize);
        }
        let changed = false;
        merge: for (const availableModules of availableModulesToBeMerged) {if (cachedMinAvailableModules === undefined) {
                cachedMinAvailableModules = availableModules;
                info.minAvailableModules = cachedMinAvailableModules;
                info.minAvailableModulesOwned = false;
                changed = true;
            } else {if (info.minAvailableModulesOwned) {//...} else if (cachedMinAvailableModules.plus === availableModules.plus) {
                    //...
                    //!!!计算并集
                    for (const m of cachedMinAvailableModules) {if (!availableModules.has(m)) {const newSet = /** @type {ModuleSetPlus} */ (new Set());
                            newSet.plus = availableModules.plus;
                            const iterator = cachedMinAvailableModules[Symbol.iterator]();
                    
                            let it;
                            while (!(it = iterator.next()).done) {
                                const module = it.value;
                                if (module === m) break;
                                newSet.add(module);
                            }
                            while (!(it = iterator.next()).done) {
                                const module = it.value;
                                if (availableModules.has(module)) {newSet.add(module);
                                }
                            }
                            info.minAvailableModulesOwned = true;
                            info.minAvailableModules = newSet;
                            changed = true;
                            continue merge;
                        }
                    }
                } else {//...}
            }
        }
        if (changed) {
            info.resultingAvailableModules = undefined;
            outdatedChunkGroupInfo.add(info);
        }
    }
    chunkGroupsForMerging.clear();};

4.7 解决 outdatedChunkGroupInfo

在经验 processQueue()->processConnectQueue()->processChunkGroupsForMerging() 的解决后,最终到 processOutdatedChunkGroupInfo() 的执行

while (queue.length || queueConnect.size) {processQueue();
    if (chunkGroupsForCombining.size > 0) {processChunkGroupsForCombining();
    }
    if (queueConnect.size > 0) {
        // calculating available modules
        processConnectQueue();

        if (chunkGroupsForMerging.size > 0) {
            // merging available modules
            processChunkGroupsForMerging();}
    }
    if (outdatedChunkGroupInfo.size > 0) {
        // check modules for revisit
        processOutdatedChunkGroupInfo();}
    if (queue.length === 0) {
        const tempQueue = queue;
        queue = queueDelayed.reverse();
        queueDelayed = tempQueue;
    }
}

processOutdatedChunkGroupInfo()的代码也很多,然而逻辑是比拟清晰易懂的,如上面所示,分为 4 个局部,因为以后异步依赖 ChunkGroupInfominAvailableModules产生了变动,导致之前解决的一些逻辑都得从新查看一遍,次要包含:

  • skippedItems: 之前因为检测到 minAvailableModules 蕴含以后 module,即 Parent Chunks 能够提供以后 module 进行复用,因而没有退出到 queue 中进行解决,当初从新检测了下,这些跳过的 module 是否还在 minAvailableModules 中,如果没有,则须要重新加入队列中进行解决
  • skippedModuleConnections:之前因为检测到 activeState 不为 true,因而退出到skippedModuleConnections,当初从新检测下状态是否产生扭转,如果产生扭转,则须要重新加入队列中进行解决
  • children chunk groups:从新将 children chunk 退出到 queueConnect 中,也就是须要计算下异步依赖的 minAvailableModules,因为异步依赖的minAvailableModules 是依靠于 parent chunk,当初parent chunkminAvailableModules产生扭转,对应的异步依赖也同样须要从新计算下minAvailableModules
  • availableChildren: 拿出以后 ChunkGroup 的子 ChunkGroup,将children 都重新加入到 chunkGroupsForCombining 从新计算下minAvailableModules
const processOutdatedChunkGroupInfo = () => {
    statChunkGroupInfoUpdated += outdatedChunkGroupInfo.size;
    // Revisit skipped elements
    for (const info of outdatedChunkGroupInfo) {
        // 1. Reconsider skipped items
        if (info.skippedItems !== undefined) {const { minAvailableModules} = info;
            for (const module of info.skippedItems) {
                if (!minAvailableModules.has(module) &&
                    !minAvailableModules.plus.has(module)
                ) {
                    queue.push({
                        action: ADD_AND_ENTER_MODULE,
                        block: module,
                        module,
                        chunk: info.chunkGroup.chunks[0],
                        chunkGroup: info.chunkGroup,
                        chunkGroupInfo: info
                    });
                    info.skippedItems.delete(module);
                }
            }
        }

        // 2. Reconsider skipped connections
        if (info.skippedModuleConnections !== undefined) {const { minAvailableModules} = info;
            for (const entry of info.skippedModuleConnections) {const [module, activeState] = entry;
                if (activeState === false) continue;
                if (activeState === true) {info.skippedModuleConnections.delete(entry);
                }
                if (
                    activeState === true &&
                    (minAvailableModules.has(module) ||
                        minAvailableModules.plus.has(module))
                ) {info.skippedItems.add(module);
                    continue;
                }
                queue.push({
                    action: activeState === true ? ADD_AND_ENTER_MODULE : PROCESS_BLOCK,
                    block: module,
                    module,
                    chunk: info.chunkGroup.chunks[0],
                    chunkGroup: info.chunkGroup,
                    chunkGroupInfo: info
                });
            }
        }

        // 2. Reconsider children chunk groups
        if (info.children !== undefined) {
            statChildChunkGroupsReconnected += info.children.size;
            for (const cgi of info.children) {let connectList = queueConnect.get(info);
                if (connectList === undefined) {connectList = new Set();
                    queueConnect.set(info, connectList);
                }
                connectList.add(cgi);
            }
        }

        // 3. Reconsider chunk groups for combining
        if (info.availableChildren !== undefined) {for (const cgi of info.availableChildren) {chunkGroupsForCombining.add(cgi);
            }
        }
    }
    outdatedChunkGroupInfo.clear();};

4.8 calculateResultingAvailableModules 详解

4.8.1 源码剖析

在下面的流程中,咱们屡次应用到 calculateResultingAvailableModules() 这个办法,它自身的代码量也很少,逻辑方面也十分直白,次要是两个公式的计算,次要是 minAvailableModules 和 minAvailableModules.plus 的比拟
resultingAvailableModules 分为两个局部

  • resultingAvailableModules = new Set():modules of chunk
  • resultingAvailableModules.plus = new Set():比拟 minAvailableModules/minAvailableModules.plus

当 minAvailableModules 的长度 <=minAvailableModules.plus 的长度时,维持 plus 不变,将 minAvailableModules 并入到 resultingAvailableModules
当 minAvailableModules 的长度 >minAvailableModules.plus 的长度,此时 plus 须要裁减,将 minAvailableModules 并入到 resultingAvailableModules.plus
因而最终的后果就是

  • resultingAvailableModules = (modules of chunk) + (minAvailableModules + minAvailableModules.plus)
  • resultingAvailableModules = (minAvailableModules + modules of chunk) + (minAvailableModules.plus)

惟一区别就是 minAvailableModules 到底是放在 resultingAvailableModules 还是 resultingAvailableModules.plus

const calculateResultingAvailableModules = chunkGroupInfo => {if (chunkGroupInfo.resultingAvailableModules)
            return chunkGroupInfo.resultingAvailableModules;

        const minAvailableModules = chunkGroupInfo.minAvailableModules;

        // Create a new Set of available modules at this point
        // We want to be as lazy as possible. There are multiple ways doing this:
        // Note that resultingAvailableModules is stored as "(a) + (b)" as it's a ModuleSetPlus
        // - resultingAvailableModules = (modules of chunk) + (minAvailableModules + minAvailableModules.plus)
        // - resultingAvailableModules = (minAvailableModules + modules of chunk) + (minAvailableModules.plus)
        // We choose one depending on the size of minAvailableModules vs minAvailableModules.plus

        let resultingAvailableModules;
        if (minAvailableModules.size > minAvailableModules.plus.size) {// resultingAvailableModules = (modules of chunk) + (minAvailableModules + minAvailableModules.plus)
            resultingAvailableModules =
                /** @type {Set<Module> & {plus: Set<Module>}} */ (new Set());
            for (const module of minAvailableModules.plus)
                minAvailableModules.add(module);
            minAvailableModules.plus = EMPTY_SET;
            resultingAvailableModules.plus = minAvailableModules;
            chunkGroupInfo.minAvailableModulesOwned = false;
        } else {// resultingAvailableModules = (minAvailableModules + modules of chunk) + (minAvailableModules.plus)
            resultingAvailableModules =
                /** @type {Set<Module> & {plus: Set<Module>}} */ (new Set(minAvailableModules)
                );
            resultingAvailableModules.plus = minAvailableModules.plus;
        }

        // add the modules from the chunk group to the set
        for (const chunk of chunkGroupInfo.chunkGroup.chunks) {for (const m of chunkGraph.getChunkModulesIterable(chunk)) {resultingAvailableModules.add(m);
            }
        }
        return (chunkGroupInfo.resultingAvailableModules =
            resultingAvailableModules);
    }

4.8.2 示例图解

5.buildChunkGraph-2-connectChunkGroups

5.1 blockConnections 数据收集

blockConnections数据在 iteratorBlock() 解决异步依赖时初始化

解决不是 Entry 类型:queueConnection 的构建
const processBlock = (block, isSrc) => {
    //... 解决同步依赖
    for (const b of block.blocks) {iteratorBlock(b);
    }
}

const iteratorBlock = b => {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
        });
    }
}

5.2 解决 blockConnections 数据,绑定 ChunkGroup

如上面代码块所示,areModulesAvailable()次要是判断该异步的 chunkGroup 所有的依赖是否都处于 parent chunkGroupresultingAvailableModules中,也就是 parent chunkGroup 的一些同步依赖曾经蕴含了异步依赖所须要的所有modules

异步依赖间接拿 parent chunkGroup 的同步依赖即可,不须要跟其余 module 建设关系

connectBlockAndChunkGroup(): 异步依赖 AsyncDependenciesBlock 跟新建设的 ChunkGroup 进行绑定
connectChunkGroupParentAndChild(): 异步依赖 ChunkGroup 跟其 parent ChunkGroup 进行绑定

const connectChunkGroups = (compilation, blocksWithNestedBlocks, blockConnections, chunkGroupInfoMap) => {const { chunkGraph} = compilation;
    // 呈现在父 chunkA 有异步依赖 chunkB,chunkB 有同步依赖 chunkC
    // 然而 chunkC 是 chunkA 的同步依赖,那么 chunkB 就跳过这个异步 chunkC 的关联
    for (const [block, connections] of blockConnections) {
        if (!blocksWithNestedBlocks.has(block) &&
            connections.every(({chunkGroup, originChunkGroupInfo}) =>
              // originChunkGroupInfo 蕴含了这个 chunkGroup 的所有 Modules
              // 阐明异步依赖 block 所在的 chunk 曾经被所在的 chunk 的父 chunk 蕴含了
                areModulesAvailable(
                    chunkGroup,
                    originChunkGroupInfo.resultingAvailableModules
                )
            )
        ) {continue;}

        for (let i = 0; i < connections.length; i++) {const { chunkGroup, originChunkGroupInfo} = connections[i];
            // 关联这个 AsyncDependenciesBlock 和 chunkGroup
            chunkGraph.connectBlockAndChunkGroup(block, chunkGroup);
            // 关联这个 chunkGroup 和它的父 chunkGroup
            connectChunkGroupParentAndChild(originChunkGroupInfo.chunkGroup, chunkGroup);
        }
    }
};

下面的剖析可能看起来有点懵,然而举一个具体的例子就能很快明确 connectChunkGroups() 的逻辑,如上面所示

  • 如果 entry1.js 没有同步依赖 async_B.js,那么因为它有异步依赖async_B.jsasync_B.js 会独自造成一个 ChunkChunkGroup
  • 然而当初 entry1.js 曾经有了同步依赖 async_B.js,那么它就没必要再让async_B.js 独自造成一个 ChunkChunkGroup,因为 entry1.js 曾经把 async_B.js 打包进去本人的 Chunk 了,而下面代码中 areModulesAvailable() 就是检测这个逻辑的具体方法,如果 originChunkGroupInfo 蕴含了这个 chunkGroup 的所有 Modules,那么这个异步ChunkGroup 就能够删除了

具体删除逻辑请看下一节的剖析

6.buildChunkGraph-3-cleanupUnconnectedGroups

革除所有没有连贯的chunkGroups

6.1 allCreatedChunkGroups 数据收集

allCreatedChunkGroups也是在解决异步依赖 iteratorBlock() 中进行数据初始化

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 类型} else if (!chunkGroupInfo.asyncChunks || !chunkGroupInfo.chunkLoading) {// 场景 2: webpack.config.js 中 asyncChunks=false/chunkLoading=false} else {
            // 场景 3: 非 Entry+ 容许 asyncChunk 的状况
            c = compilation.addChunkInGroup(
                b.groupOptions || b.chunkName,
                module,
                b.loc,
                b.request
            );
            blockConnections.set(b, []);
            allCreatedChunkGroups.add(c);
        }
    } 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);
    }
}

6.2 allCreatedChunkGroups 数据处理

通过 chunkGroup.getNumberOfParents() 检测异步 ChunkGroup 是否没有关联其Parent Chunk,如果没有关联,间接革除该ChunkGroup

const cleanupUnconnectedGroups = (compilation, allCreatedChunkGroups) => {const { chunkGraph} = compilation;

    for (const chunkGroup of allCreatedChunkGroups) {
    // 清理依赖,如果这个 chunkGroup 的父 chunk 为 0,阐明没有连贯,间接革除
        if (chunkGroup.getNumberOfParents() === 0) {for (const chunk of chunkGroup.chunks) {compilation.chunks.delete(chunk);
                chunkGraph.disconnectChunk(chunk);
            }
            chunkGraph.disconnectChunkGroup(chunkGroup);
            chunkGroup.remove();}
    }
};

如上面所示,当 entry1.js 曾经有了同步依赖 async_B.js,那么它就没必要再让async_B.js 独自造成一个 ChunkChunkGroup,因而在下面 connectChunkGroups() 中不会进行 connectChunkGroupParentAndChild(originChunkGroupInfo.chunkGroup, chunkGroup) 关联 ChunkGroup 之间的关系,因而会导致异步依赖 async_B.js 对应的 ChunkGroup.getNumberOfParents() === 0,最终触发ChunkGroup 删除逻辑,移除该ChunkGroup

7.hooks.optimizeChunks

while (this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups)) {/* empty */}

在通过 visitModules() 解决后,会调用 hooks.optimizeChunks.call() 进行 chunks 的优化,如下图所示,会触发多个 Plugin 执行,其中咱们最相熟的就是 SplitChunksPlugin 插件

因为篇幅起因,具体分析请看下一篇文章《「Webpack5 源码」seal 阶段剖析(二)》

参考

  1. 精通 Webpack 外围原理专栏
  2. webpack@4.46.0 源码剖析 专栏
  3. webpack5 源码详解 – 封装模块

其它工程化文章

  1. 「Webpack5 源码」热更新 HRM 流程浅析
  2. 「Webpack5 源码」make 阶段(流程图)剖析
  3. 「Webpack5 源码」enhanced-resolve 门路解析库源码剖析
  4. 「vite4 源码」dev 模式整体流程浅析(一)
  5. 「vite4 源码」dev 模式整体流程浅析(二)

正文完
 0