多进程打包:thread-loader 源码(13)
码字不易,感谢阅读,你的点赞让人振奋!如文章有误请移步评论区告知,万分感谢!未经授权不得转载!
一、前情回顾
上文详细讨论了 onMessage 的代码结构和代码的意义,另外讨论了其中的细节问题如下:
-
onMessage分为三种类型的消息:- 1.1 代表任务的
job,任务push到queue - 1.2 代表结果的
result,接收父进程的返回结果 - 1.3 代表预热的
warmup,把需要的模块加载到子进程节约时间;
- 1.1 代表任务的
-
创建
queue时传入的worker函数负责跑loader,worker函数细节并未展开讨论; -
type为result是进程间通信的一环,子进程中的loaders需要了某些方法,碍于进程通信无法传递方法,所以委托父进程去调用,再通过回调把结果给到子进程;
本篇小作文正式讲解跑 loader 的 worker 函数的具体实现细节!
二、asyncQueue 的 worker 函数
asyncQueue 来自 neo-async/queue.js,老相识了,后面要说的是它的第一个参数 worker 函数,也是 thread-loader 中最终运行 loader 的函数了。
2.1 回顾 neo-async/queue.js
在讨论父进程代码中 WorkerPool 的时候详细讲述过 neo-async/queue 的工作原理,他接收一个 worker 函数和一个表示并发数目的数字,返回一个队列 queue。
当有数据被 push 进 queue 时,neo-async 会调用 runQueue 方法消耗队列,runQueue 内部会调用创建 queue 时传入 worker 函数处理 data,并且给 worker 函数传入一个 done 方法,这个 done 将会在 worker 函数执行到结束时调用,意在告知 queue 本次 worker 函数执行已经结束。
在调用 runQueue 的过程中需要判断当前已经在运行的任务是否超出创建 queue 时传入的并发数限制,如果超过了就会暂停。
const queue = asyncQueue(({ id, data }, taskCallback) => {
// 这个箭头函数就是 worker 函数了
// taskCallback 就是 runQueue 的 done 方法
}, PARALLEL_JOBS);
关于 neo-async/queue 暂时就说这么多,本文的重点是 worker 函数;
2.2 worker 函数代码结构
2.2.1 参数:
{ id, data },这个位置的参数就是前面push到queue里面的data;taskCallback是runQueue里面传入的done
2.2.2 方法内部逻辑:
- 创建
resolveWithOptions方法,这个方法是下面runLoaders的loaderContext.resolve方法的实现; - 声明常量
buildDependencies数组 - 调用
loaderRunner.runLoaders方法,传入runLoaders所需参数(包含loaderContext对象)、接收loader结果的回调函数,这些参数后面会详细讨论;
const queue = asyncQueue(({ id, data }, taskCallback) => {
// taskCallback 就是 runQueue 的 done
try {
// loaderContext.resolve 方法的实现
const resolveWithOptions = (context, request, callback, options) => {};
// 保存本次 runLoaders 得到的 buildDependencies
const buildDependencies = [];
// 调用 loaderRunner.runLoaders 跑 loaders
loaderRunner.runLoaders(
{
// runLoaders 所需的选项对象,包含一个模拟出来的 loaderContext
loaders: data.loaders,
context: { /* 模拟出来的 loaderContext */ }
},
(err, lrResult) => {
// 处理 loader 运行结束后的结果
// 这个函数给他取个名字,后面叫他 loader 结果回调
}
);
} catch (e) {
taskCallback();
}
}, PARALLEL_JOBS);
三、 runLoader 方法
exports.runLoaders = function runLoaders(options, callback) {
var loaderContext = options.context || {};
// 扩展 loaderContext
loaderContext.dependency = loaderContext.addDependencies = function addDependency () {
}
// ....
// 调用 iteratePitchingLoaders 加载并运行 loader 的 pitch 方法,
// 进入 pitch 阶段;
// pitch 阶段结束后自动进入 normal 阶段,
// 结束后调用 callback 回调即 loader 结果回调
iteratePitchingLoaders(processOptions, loaderContext, function (err, result) {
// 调用 runLoaders 回调传入 result
callback(null, {
result: result,
resourceBuffer: processOptions.resourceBuffer,
cacheable: requestCacheable,
fileDependencies: fileDependencies,
contextDependencies: contextDependencies
});
}
}
3.1 webpack 调用 runLoaders
之所以提这个点,也是便于大家理解 worker 做这些工作的初衷,下图是 webpack 内部调用的 runLoaders 方法,图中的 context: loaderContext 就是 webpack 内部初始化的 loaderContext 这个上下文对象,也就是 loader 函数内部 this 所绑定的对象,上文档传送门

3.2 worker.js 中的 loaderContext vs webpack 的 loaderContext
大家思考一个问题,为什么在这里调用 runLoader 传入的 context 是一个新构造的对象,而不是 webpack 中的 loaderContext 对象(在作用上这两个对象时等价的)?
如果你很快就反应出答案,说明前面的内容你已经滚瓜烂熟了:是因为在 thread-loader 中调用 runLoaders 这个方法是在 worker.js 中调用的,而 worker.js 又是在子进程中的调用。碍于进程间通信的限制,进程间通信使用的自定义管道的方式实现的,而这种实现方式传递的是被序列化的 JSON 字符串。
这就导致 webpack 中的 loaderContext 对象无法被传递到子进程中,究其根本,是因为进程间的内存是隔离的,webpack 的 loaderContext 对象存在于父进程,而 runLaoders 却是在子进程中。所以当子进程需要时,只能再造一个新的对象。
这个新造的对象包含了 loaderContext 应有的属性和方法,但是这些方法并不直接处理工作,而是转发这些工作到父进程让父进程完成,父进程完成后把结果发送给子进程。
这里就揭示了这个全新的 loaderContext 的核心实现,虽然这里没有代码,但是请记住,这个核心:转发工作个父进程,等待接收父进程传送来的结果。
3.3 webpack loaderContext
这里就偷个懒上个截图吧,只需要关注一些方法和属性,后面的 worker.js 会同样实现一份这样的方法和属性出来;

3.4 worker.js loaderContext
let cfg = {
loaders: data.loaders, // 要跑的 loader
resource: data.resource, //
readResource: fs.readFile.bind(fs),
context: { // worker.js 的 loaderContext 对象
version: 2,
fs,
// 模拟 loaderContext 的 loadModule 方法
loadModule: (request, callback) => {},
// 模拟 loaderContext 的 resolve 方法
resolve: (context, request, callback) => {},
// 模拟 loaderContext 的 getResolve 方法
getResolve: (options) => (context, request, callback) => {},
// 模拟 loaderContext 的 getOptions 方法
getOptions(schema) {},
// 模拟 loaderContext 的 emitWarning 方法
emitWarning: (warning) => {},
// 模拟 loaderContext 的 emitError 方法
emitError: (error) => {},
// 模拟 loaderContext 的 exec 方法
exec: (code, filename) => {},
// 模拟 loaderContext addBuildDependency 方法
addBuildDependency: (filename) => {},
options: {},
webpack: true,
'thread-loader': true,
sourceMap: data.sourceMap,
target: data.target,
minimize: data.minimize,
resourceQuery: data.resourceQuery,
rootContext: data.rootContext
},
}
四、总结
本篇小作文讨论了一下 worker.js 中以下功能:
- 用于控制并发创建的 queue 的 worker 函数的代码结构和大致功能;
- 另外还讨论了
runLoaders方法,接收options和callback(loader结果函数); - 期间还讨论了
loaderContext作用,还对比了worker.js的loaderContext对象和webpack的loaderContext对象; - 借助两个
loaderContext回顾了进程间通信,还铺垫了worker.js中实现的loaderContext上的方法核心;
转载自:https://juejin.cn/post/7108274196815282183