乐趣区

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

本文内容基于 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 模式整体流程浅析(二)
退出移动版