来看 webpack5 模块创建过程啊
一、前文回顾
上文讲述了 webpack 处理入口模块的创建工作,主要复习compilation 和 compiler 协同启动模块构建工作:
- 复用 EntryPlugin 的注册过程及关键 compiler.hooks.make,此时 EntryPlugin 调用 compilation.addEntry 方法传入入口;
- 学习了 compilation.addEntry 方法作用,其核心调用 compilation._addEntryItem 方法加入 entry 项;
- 学习了 compilation._addEntryItem 方法,其内部处理 entryData 并调用 compilation.addModuleTree 加入入口模块,入口模块相当于 ModuleTree 的 root 节点;
- 学习了 compilation.addModuleTree 方法内部则是调用 compilation._handleModuleCreation 方法开始处理入口模块及其依赖的子模块的创建工作;
今天这篇小作文,即将进入 compilation._handleModuleCreation 探寻 webpack 宇宙的模块创建星球!
二、compilation.handleModuleCreation
2.1 参数(或参数结构参数)
- factory: 模块的工厂函数
- dependencies: 当前模块的依赖,对于入口来说 是EntryDependency 类型,对于后续解析的依赖模块则是 parse 过程中所得到的各式各样的 Dependency,也就是大家所说的
递归解析依赖
中的依赖
- originModle: 忽略
- contextInfo: 忽略
- context: 项目目录
- recursive: true,是否递归
- connectOrigin: true, 是否建立元模块的链接;
- callback:addModuleTree 收到的 callback;
以下是实参:
2.2 代码逻辑
该方法处理模块创建工作,所谓模块的创建则是广义上的,这个过程中也包含了模块的构建、子模块依赖解析及创建构建工作,当然这个方法是个封装调度的角色;
当然,这些不全是由该方法实现,这些流程依赖 compilation 的 4 个队列及其 processor 的协助;这里先不展开,先来宏观看下这个流程;
- 调用 compilation.factorizeModule 方法,该方法把待处理的模块信息加入到 compilation.factorizeQueue 中,加入队列就会被队列的 processor 处理,这个过程主要是调用 factory 创建模块;
- 这第二步来到了 compilation.factorizeModule 回调,在回调用调用 compilation.addModule 方法把上一步得到的模块加入到 addModuleQueue 中,addModuleQueue 的 processor 的作用则是构建 ModuleGraph;
- 第三步来到了 compilation.addModule 的回调,该回调中调用 compilation._handleModuleBuildAndDependencies 方法处理模块的依赖模块们;
class Compilation {
handleModuleCreation(
{
factory,
dependencies,
originModule,
contextInfo,
context,
recursive = true,
connectOrigin = recursive
},
callback
) {
// ...
// 1.
this.factorizeModule(
{
currentProfile,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
(err, factoryResult) => {
// 2.
this.addModule(newModule, (err, module) => {
// 3.
this._handleModuleBuildAndDependencies(
originModule,
module,
recursive,
callback
);
});
});
}
}
三、compilation.factorizeModule 和 factorizeQueue
compilation.factorizeModule 作用很简单,把数据加入队列就好了。剩下的工作都交给队列的 _processor 处理;
Compilation.prototype.factorizeModule = function (options, callback) {
this.factorizeQueue.add(options, callback);
}
3.1 方法参数
- options: 创建模块所需的原始数据,包括模块的工厂函数等;
- callback: 回调函数,接收异步队列 processor 处理结果的回调函数,这里就是 compilation.handleModuleCreation 调用 factorizeModule 传入的回调(调用 addModule 的那个)
3.2 方法逻辑
方法的功能很简单,把接收到的数据加入到 factorizeQueue 中,如下:
this.factorizeModule(
{ // 这个对象
currentProfile,
factory,
dependencies,
factoryResult: true,
originModule,
contextInfo,
context
},
数据加入到 factorizeQueue 中,会触发 factorizeQueue 定义时传入的队列消费函数 processor 方法 this._factorizeModule
:
3.3 compilation.factorizeQueue
factorizeQueue 是负责并发创建模块的队列,实例化自 AsyncQueue 类型。队列这个数据结构在 webpack v5 中广泛应用,从这里开始就需要有意识的去理解队列这个数据结构。
this.factorizeQueue = new AsyncQueue({
name: "factorize",
parent: this.addModuleQueue,
processor: this._factorizeModule.bind(this) // 这个就是队列的消费函数 processor
});
队列的 add 方法负责将待处理数据加入到队列中,随后它的 processor 函数将会消费队列;
当向 compilation.factorizeQueue 中添加项目时,即 compilation.factorizeQueue.add() 方法被调用时,则 AsyncQueue 会自动触发这个队列的 processor —— compilation._factorizeModule 方法消耗队列;
3.3.1 parent 队列
父亲队列:该队列的 parent (父亲/亲代)队列为 compilation.addModuleQueue 队列;
3.3.2 processor
下面我们大致看看这个 compilation._factorizeModule 方法;
class Compilation {
// ....
_factorizeModule({ factory, /* .... */ }, callback) {
factory.create(
{ /* .... */ },
(err, result) => {
if (!factoryResult) {
const {
fileDependencies,
contextDependencies,
missingDependencies
} = result;
if (fileDependencies) {
this.fileDependencies.addAll(fileDependencies);
}
if (contextDependencies) {
this.contextDependencies.addAll(contextDependencies);
}
if (missingDependencies) {
this.missingDependencies.addAll(missingDependencies);
}
}
callback(null, factoryResult ? result : result.module);
}
);
}
}
该方法内部调用 factory.create 方法进行模块的创建,这个过程是把各种 webpack 内部引用的 资源
转变成 webpack 内部统一的 模块(Module)
。
factory 则是创建模块的工厂,不同类型模块对应不同的工厂,一般在 webpack 内部都是 NormalModuleFactoy 这个工厂,当然如果是上下文的则是 ContextModuleFactory 工厂。
四、compilation.addModule 和 addModuleQueue
compilation.addModule 方法负责将由 factorizeModule 方法新创建的模块加入到 addModuleQueue 中,然后由 addModuleQueue 的 processor 即 compilation._addModule 方法把新创建的模块加入到 compilation.modules 中和 compilation._modules 中。
class Compilation {
addModule(module, callback) {
this.addModuleQueue.add(module, callback);
}
}
4.1 参数
- module: 模块对象,是有前面的 _factorizeModule 方法创建所得;
- callback: 用于接收当前 addModuleQueue 队列的处理结果的回调函数;
4.2 addModuleQueue
this.addModuleQueue = new AsyncQueue({
name: "addModule",
parent: this.processDependenciesQueue,
getKey: module => module.identifier(),
processor: this._addModule.bind(this)
});
4.2.1 parent
addModule 的 parent 队列为 compilation.processDependenciesQueue。
4.2.2 processor
简化后的代码如下:
class Compilation {
_addModule(module, callback) {
const identifier = module.identifier();
this._modulesCache.get(identifier, null, (err, cacheModule) => {
// 加入到 compilation._modules 和 modules 上
this._modules.set(identifier, module);
this.modules.add(module);
callback(null, module);
});
}
}
当调用 addModuleQueue.add() 方法向 addModuleQueue 中加入数据时,会被当前的 _addModule 方法处理。
最后调用 callback 回调,callback 则是加入队列时声明的函数,我们来看 addModuleQueue.add 时传入的回调函数(见下面代码 注释 1.
):
this.addModule(newModule, (err, module) => {
// 1. 这个回调就是了
this._handleModuleBuildAndDependencies(
originModule,
module,
recursive,
callback
);
});
可以非常清晰看到,在得到 addModule 的结果后调用了 compilation._handleModuleBuildAndDependencies 方法,下面我们来看这个方法做了什么!
五、compilation._handleModuleBuildAndDependencies
方法顾名思义,处理模块的构建和模块的依赖,下面是简化后的代码
class Compilation {
_handleModuleBuildAndDependencies(originModule, module, recursive, callback) {
// ....
this.buildModule(module, err => {
this.processModuleDependencies(module, err => {
callback(null, module);
});
});
}
}
从上面的代码上可以很清晰的看出最重要的部分:在最后调用了 compilation.buildModule 方法,该方法用于处理模块的构建,下面我们看看这个方法如何实现的模块构建
六、compilation.buildModule 和 buildQueue
compilation.buildModule 方法是在前面 compilation._handleModuleBuildAndDependencies 方法中调用的。
根据我们前面已经获得的经验可知,webpack 内部会把这个待构建的模块以及处理构建后的模块的结果回调加入到相应的队列中,代码如下:
class Compilation {
buildModule(module, callback) {
this.buildQueue.add(module, callback);
}
}
6.1 参数:
- module:由前面 factorizeQueue 创建,并经由 addModuleQueue 加入到到 compilation.modules 后的模块;
- callback:受理构建结果的回调;
6.2 buildQueue
webpack 中处理模块构建的队列,
this.buildQueue = new AsyncQueue({
name: "build",
parent: this.factorizeQueue,
processor: this._buildModule.bind(this)
});
6.2.1 parent
buildQueue 的 parent 是 factorizeQueue 队列,factorizeQueue 的作用前面已经详述过。
6.2.2 processor
该队列的 prcessor 为 _buildModule 方法,该方法用于处理模块的构建相关工作,所谓构建可以粗暴的理解为 跑 loader。
class Compilation {
_buildModule(module, callback) {
// 1.
module.needBuild(
{
/* ... */
},
(err, needBuild) => {
// 2.
this.hooks.buildModule.call(module);
this.builtModules.add(module);
// 4.
module.build(
this.options,
this,
this.resolverFactory.get("normal", module.resolveOptions),
this.inputFileSystem,
err => {
this._modulesCache.store(module.identifier(), null, module, err => {
// 5.
this.hooks.succeedModule.call(module);
return callback();
});
}
);
}
);
}
}
当有数据加入到 compilation.buildQueue 队列中,buildQueue 的 processor _buildQueue 会在调用 module.needBuild 先判断是否需要构建,这一步主要是读取持久化缓存。
若得到 needBuild 方法的肯定后,则调用 modoule.build 方法进行构建,并在 module.build 的结果回调中调用 callback,这个 callback 是上面 compilation._handleModuleBuildAndDependcencies 方法调用 this.buildModule 方法时传入的:
this.buildModule(module, err => {
// 就是这个回调了
this.processModuleDependencies(module, err => {
callback(null, module);
});
});
从代码上可以很清晰的看出回调中调用了 processModuleDependencies 方法,下面我们看看这个方法有啥用
七、compilation.processModuleDependencies 和 processDependenciesQueue
compilation.processModuleDependencies 在模块构建后被调用,方法顾名思义,处理模块的依赖。这个过程肯定还是会依赖一个队列 processDependenciesQueue ,下面咱们看看!
class Compilation {
processModuleDependencies(module, callback) {
this.processDependenciesQueue.add(module, callback);
}
}
7.1 参数
- module: 经由 buildQueue 构建过的 module 对象;
- callback:受理 processor 结果的回调函数;
7.2 processDependenciesQueue
该队列用于处理构建后模块的依赖;
this.processDependenciesQueue = new AsyncQueue({
name: "processDependencies",
parallelism: options.parallelism || 100,
processor: this._processModuleDependencies.bind(this)
});
7.2.1 parent
注意该队列无 parent 属性,在 AsyncQueue 创建中,不传递 parent 的队列实例将会成为后续指定该队列(或其子队列)为 parent 队列的 root 。
7.2.2 processor
该队列的消费函数为 compilation._processModuleDependencies 方法:
class Complilation {
_processModuleDependencies(module, callback) {
// 1.
const onDependenciesSorted = err => {
for (const item of sortedDependencies) {
// 注意这里!!!
// 调用 handleModuleCreatetion
this.handleModuleCreation(item, err => { /* */ });
}
};
// 2.
const processDependency = (dep, index) => {
// 简化 onDpendenciesSorted 调用
onDependenciesSorted();
});
// 3.
const queue = [module];
do {
const block = queue.pop();
if (block.dependencies) {
currentBlock = block;
let i = 0;
for (const dep of block.dependencies) processDependency(dep, i++);
}
// 异步引用的是 block,比如 import(b.js).then 这种
if (block.blocks) {
// 异步模块重新加入队列
for (const b of block.blocks) queue.push(b);
}
} while (queue.length !== 0);
}
}
这个 processor 是这些里面最复杂的一个了,尽管我简化了很多细节,但是这个代码篇幅上还是有些长,我已经尽力了(😂),下面看看这里细节过程:
- 声明内部函数 onDependenciesSorted,该方法内部调用 this.handleModuleCreation 方法把当前依赖推入 factozizeQueue;
- 声明内部方法 processDependency,该方法负责调用 1. 中的 onDependenciesSorted 方法;
- 最后则是用一个队列模拟递归调用 2. 中的 processDependency 方法;
有没有感觉到这是个循环,把构建得到的依赖再次加入到 factorizeQueue 中走一遍流程:模块创建 -> add -> build -> processDependency
这样一个循环;
八、总结
本篇小作文讨论了 webpack 递归解析依赖的主流程框架,整个过程如下:
-
调用 handleModuleCreation 方法开启模块的创建;
-
在 handleModuleCreation 中调用了 factorizeModule 方法;
-
factorizeModule 方法的作用仅仅是把待创建的模块和处理创建模块后的回调加入到 factorizeQueue 中;
- parent: factorizeQueue 的 parent 队列是 addModuleQueue;
- processor: factorizeQueue 的 processor 是 _factorizeModule 方法,该方法的核心是调用 dep.factory.create 完成模块的创建(Module);
-
在得到 上一步 创建的模块后,调用 addModule 方法,将模块加入到 factorizeQueue 的 parent 队列 —— addModuleQueue 中;
- parent: addModuleQueue 的 parent 队列是 processDependenciesQueue;
- processor: addModuleQueue 的 processor 是 _addModule 方法,该方法的作用是把新创建的模块保存到 compilation.modules 和 compilation._modules 中,完成保存动作后调用 回调 就会调用 _handleModuleBuildAndDependencies;
-
_handleModuleBuildAndDependencies,内部逻辑调用了 buildModule 方法构建模块;buidModule 内部仅仅是将待构建的模块及回调加入到 buildQueue 中;
- parent: factorizeQueue 队列,负责模块的创建的 队列;
- processor:_buildModule 方法,该方法负责调用 module.build 进行模块 build,所谓 build 就是 跑loader,并且在拿到结果后调用回调,即调用 processModuleDependencies 方法处理模块依赖;
-
processModuleDependencies 方法的作用就是将待处理的模块和结果受理回调加入到 processDependenciesQueue 队列中;
- parent: 无,processDependenciesQueue 是 root 队列;
- processor:_processModuleDependencies 方法,该方法的主要作用是为
模块的同步和异步依赖
调用 compilation.handleModuleCreation 方法完成递归;
这里这么梳理后是不是通透多了呢?但是想想是不是关键环节还差点呢?是的,下一篇小作文将集中讲解这个递归到底是怎么实现的!
转载自:https://juejin.cn/post/7340295805999546402