多进程打包: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