持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的22天,点击查看活动详情
码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
未经授权不得转载!
一、前情回顾
上一篇小作文详细讨论了子进程 runLoaders 的结果回调的逻辑:
- 得到 runLoaders 的结果中的 result 和各种依赖数组;
- 转换 result 数组为 convertedResult,这个过程中会把 result 中的 buffer、字符串变成 buffer 并添加描述信息,这些描述信息用于父进程从管道中读取 buffer 并还原 result,result 中非 buffer 或字符串的原样保留;
- 向父进程发送 type: job 的消息,消息包含各段 buffer 长度的 data 数组、描述loader 运行 result 的描述信息 result(实为 convertedResult 数组),父进程则触发 onWorkerMessage 处理,处理过程利用 asyncMapSeries 遍历 buffer 长度数组拼接 buffer,最后还原 loader 的 result 结果并调用回 jobCallback 回调;
本篇接着写 jobCallback 以及 jobCallback 的工作细节;
二、jobCallback 的由来
通过前面的讲述,jobCallback 是在子进程 worker 得到 runLoaders 的结果后向管道写入 type: job 的消息后,父进程调用 onWorkerMessage 处理 type 为 job 的消息时从 this.jobs[id] 获取的,代码如下:
class PoolWorker {
onWorkerMessage () {
case job:
const { callback: jobCallback } = this.jobs[id];
// ....
}
}
接下来的问题就是看看 this.jobs 里面的 callback 是哪里来的。给 this.jobs 上缓存 data 对象和 callback 函数的动作是在 PoolWorker.prototype.run 方法里完成的:
class PoolWorker {
run(data, callback) {
const jobId = this.nextJobId; // jobId
this.nextJobId += 1;
// 这里就是向 this.jobs 中缓存 data 和 callback 了
this.jobs[jobId] = { data, callback };
// ...
}
}
2.1 run 调用
接着思考 run 方法是哪里被调用的?PoolWorker.prototype.run 方法是在 WorkerPool.prototype.distributeJob 方法中调用的:
class WorkerPool {
distributeJob(data, callback) {
// 创建 PoolWorker 实例
const newWorker = this.createWorker();
// 调用 PoolWorker.prototype.run
newWorker.run(data, callback);
}
createWorker() {
// 创建 PoolWorker 实例
const newWorker = new PoolWorker();
return newWorker;
}
}
2.2 distributeJob 调用
接下来就是另一个好问题,distributeJob 是谁调用的,data 和 callback 又是哪儿来的?
先说第一个问题——distributeJob 是谁调用的?
distributeJob 是 WorkerPool 实例的 poolQueue 属性的 worker 函数:
export default class WorkerPool {
constructor(options) {
this.poolQueue = asyncQueue(
this.distributeJob.bind(this), // distributeJob 作为 asyncQueue 的 worker
options.poolParallelJobs
);
}
}
asyncQueue 是 neo-async 这个库的方法,传入一个 worker 和并发数,返回一个队列 poolQueue。当调用 poolQueue.push(data, cb) 方法的时候,asyncQueue 内部会调用 worker 函数(这个 worker 函数就是 distributeJob)处理处理 data;
所以这就很明显了,WorkerPool.prototype.distributeJob 是间接地被 poolQueue.push(data, cb) 调用,被 runQueue 方法直接调用:
// node_modules/neo-async
function runQueue() {
while (!q.paused && workers < q.concurrency && q._tasks.length) {
var done = _next(q, [task]);
// worker 就是 distributeJob
// task.data 是调用 poolQueue.push(data, ...) 传入的 data
// done 是传给 worker 的第二个参数回调
worker(task.data, done);
}
}
现在说第二个问题:data 和 callback 又是哪里来的?
data 很简单,是我们调用 workerPool.run(data, callback) 方法时执行 poolQueue.push(data, callback) 传入的 data;
distributeJob 中的 data 就是这里传入的 data,但 distributeJob 接收到的 callback 是 done 方法,这一点从上面的代码块的 worker(task.data, done) 可以得出;
那么 poolQueue.push(data, callback) 传入的 callback 又做什么用呢?这里简单提一下,push 进去的 callback 被 neo-async 保存了,存在 task.callback 属性上,当 done 方法被触发时会调用 task.callback;
// poolQueue.push 方法
function push(tasks, callback) {
_insert(tasks, callback);
}
function _insert(tasks, callback, unshift) {
_callback = callback; // 缓存 callback 到变量 _callback
arrayEachSync(_tasks, _exec);
}
function arrayEachSync(array, iterator) {
// iterator 就是 _exec
while (++index < size) {
iterator(array[index], index);
}
return array;
}
function _exec(task) {
// 缓存 push 来的 data 和 callback
var item = {
data: task,
callback: _callback // 缓存 push 过来的回调
};
q._tasks.push(item);
}
2.3 done 方法
done 方法是 runQueue 方法的私有方法,是执行 _next 方法得到的返回值,这个 done 也就是这个部分的答案了,done 就是 jobCallback 方法
// node_modules/neo-async
function runQueue() {
while (!q.paused && workers < q.concurrency && q._tasks.length) {
// _next 方法的返回值就是 done
var done = _next(q, [task]);
// done 是传给 worker 的第二个参数回调
worker(task.data, done);
}
}
2.4 _next 方法
这个方法源代码太复杂了,我们简化一下,只留下回调被调用的过程:
function _next(q, tasks) {
return function done(err, res) {
while (++taskIndex < taskSize) {
if (useApply) {
// 调用 poolQueue.push(data, callback) 传入的 callback
task.callback.apply(task, args);
} else {
task.callback(err, res);
}
}
};
}
2.5 jobCallback 总结
通过对前面的分析,jobCallback 是 runQueue 的内部方法 done,done 负责调用 push 到 poolQueue 中的 callback,而这个 callback 才是最终的一个接收 loaders 运行结果并添加依赖的回调函数。
-> thread-loader.pitch()
-> workerPool = new WorkerPool()
-> poolQueue = asynQueue(this.distributeJob.bind(this), ...)
-> workerPool.push(data, cb)
-> neo-async/runQueue
-> this.distributeJob(data, done)
-> newWorker = new PoolWorker()
-> child_spawn(node, dist/worker.js, { stdio: [,,, pipe, pipe]})
-> queue = asyncQueue(runLoaders) =>
runLoaders 回调执行 writeJson({ type: 'job', result })
-> readNextMessage
-> onMessage
-> case: 'job'
queue.push(data)
-> newWorker.readPipe/writePipe 事件监听
-> newWorker.readNextMessage
-> newWorker.onWorkerMessage
-> case: 'job'
-> jobCallback() = done()
-> cb()
-> newWorker.run(data, callback) callback 是 done 方法
-> newWorker.jobs[id] = { data, callback }
-> newWorker.writeJson({ type: job, id: jobId })
涉及到进程通信,仅凭调用链路梳理不容易理解,这里附一张图,这张图可以更清晰的看出 runLoaders 后子进程如何把结果发送给父进程的:
三、总结
本篇小作文是这个系列关于源码部分的最后一篇,零零总总的写了 17 篇,文章字数故意要和 thread-loader 源码一比一了,这是个把一本书读厚的过程,至于如何把他读薄是见仁见智的事情,现在回顾一下 thread-loader 的整个工作流程:
- thread-loader 的 pitch 方法拦截它后面的所有 loader;
- 创建 WorkerPool 实例 workerPool,它是个进程池子,用以调度进程;调度工作依赖使用 neo-async/queue.js 创建的 poolQueue 队列;
- poolQueue.push(data, callback);
- poolQueue.push 后会执行 poolQueue 的 worker 函数 —— distributeJob 创建子进程;
- distributeJob 创建子进程,通过自定义管道通信,利用 readPipe 接收子进程消息,利用 writePipe 向子进程发送消息,通信的数据载体是 JSON 格式字符串;
- 子进程接收来自父进程发送过来的消息运行 loader,碍于进程间通信限制,子进程自己构造了一个 loaderContext 对象,当用到父进程 loaderContext 中的方法时,构造的 loaderContext 对象会通过进程间通信委托父进程实现;
- 当子进程完成 runLoaders 工作后,在回调中利用管道向父进程发送结果;
- 父进程收到消息后,找到本次运行 loader 时对应的回调函数,在回调函数中把这些结果 —— 各种类型的依赖,添加到构建中;
四、致谢
感谢一路走来的各位伙伴,坚持就是胜利!
没有这个系列的写作,看源码就更加的枯燥乏味!