持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的14天,点击查看活动详情
码字不易,喜欢点赞,不喜请移步评论区告知,未经授权不得转载
一、前情回顾
上一篇小作文详细讨论 neo-async/mapSeries 方法的原理:
-
接收
collection对象,可以是对象、数组或者其他部署了迭代器接口的对象; -
当
collection类型为对象或者数组时,这个时候根据iterator.length重载iterator,即iterateor是否需要对象的key(或数组索引) -
done方法是最终结果result数组的保证的核心,再它的内部维护completed索引累加,当done被掉用时才会开启下一次的iterator迭代调用。另外,done可以确保result中顺序与iterator执行的顺序有关且与各次iterator中异步完成顺序无关。
从本篇小作文开始,我们就要进入到子进程的代码学习。thread-loader 难点在于父子进程通信。我们目前梳理这个源码,和浅羲 Vue 的源码一样,是按照代码的执行顺序组织这一系列的小作文的。这看起来会过于详细,以至于有点啰嗦了,但是这也是我写源码文章和市面上不同之处,这能保证你一定能看懂。
二、调用链路梳理
到这里,this.onWorkerMessage 是父进程收到子进程发来的消息时的处理逻辑,它的执行标志着创建 this.readNextMessage 方法的调用结束,同时也是 PoolWorker 构造函数执行的结束,也就是说此时已经得到了 PoolWorker 实例。
webpack 执行
-> thread-loader.pitch()
-> workerPool.run(data, cb)
-> this.poolQueue.push(data, cb)
-> neo-async/queue.js/runQueue()
-> WorkerPool.prototype.distributeJob()
-> WorkerPool.prototype.createWorker()
-> new PoolWorker()
-> childProcess.spawn(worker.js) 衍生子进程
-> PoolWorker.prototype.readNextMessage()
-> this.readBuffer(4, (lengthBuffer) => {
// 读 message 长度
this.readBuffer(length, (messageBuffer) => {
// 读 message 数据
this.onWorkerMessage(message)
})
})
<- 推出 PoolWorker 执行栈
<- WorkerPool.prototype.createWorker:return newWorker 推出执行栈
<- WorkerPool.prototype.distributeJob:newWorker.run(data, callback)
-> PoolWorker.prototype.run() 执行
从上的调用链路可以看出,创举 PoolWorker 实例的方法 WorkerPool.prototype.createWorker 是在 WorkerPool.prototype.distributeJob 中被调用,得到 PoolWorker 实例后紧接着调用了 newWorker.run(data, callback),这个 run 方法即 PoolWorker.prototype.run,大致代码如下:
class WorkerPool {
distributeJob (data, callback)
// ....
const newWorker = this.createWorker()
newWorker.run(data, callback); // callback 不是 cb,而是 done
}
}
// PoolWorker.prototype.run 方法
class PoolWorker {
run (data, callback) {
// run 方法通过 wirtePipe 向管道写入数据
}
}
三、PoolWorker.prototype.run
-
方法位置:
thread-loader/src/WorkerPool.js -> class PoolWorker -> run -
方法参数:
- 2.1
data:数据对象,要传递给子进程处理的信息。这data就是我们的老相识data对象,是在thread-loader的pitch方法中调用workerPool.run(data, cb)传入的,这个data类似webpack中loaderContext对象,包含loaders属性,这个loaders的值是除了thread-loaders以外的loader。除loaders属性还有resolve、getResolve、loadModuele等方法 - 2.2
callback:回调函数,run执行结束后要执行的方法。上面的data参数虽然是最初的data对象,但是这里callback却不是cb,callback是neo-async中的runQueue传入的私有方法done,是处neo-async的并发逻辑的。
- 2.1
-
方法作用:
- 3.1 维护
nextJobId累加,缓存上一次的nextJobId作为本次job的的id,以jobId为key,将本次run方法接收的data和callback组成的对象缓存到this.jobs - 3.2 通过
this.writeJson方法将本次job写入用于父子进程通信的writePipe管道,被写入的值是个对象,包含type/id/data三个属性。通过writeJson名字可以看出,并没有直接把对象传给writePipe,而是传递的序列化的json对象;
- 3.1 维护
class PoolWorker {
constructor(options, onJobDone) {}
// run 方法
run(data, callback) {
const jobId = this.nextJobId;
this.nextJobId += 1;
// 通过 jobId 缓存 data、callback
this.jobs[jobId] = { data, callback };
this.activeJobs += 1;
// 写入 writePipe
this.writeJson({
type: 'job',
id: jobId,
data,
});
}
}
四、PoolWorker.prototype.writeJson
-
方法位置:
thread-loader/src/WorkerPool.js -> class PoolWorker -> writeJson -
方法参数:
data,需要通过writePipe写入给子进程的数据,这里面包含的是需要给子进程运行的loader及运行loader需要的一些数据、配置等 -
方法作用:
- 3.1 准备四个字节的
buffer:lengthBuffer,这个buffer将来要写入的是data被序列化之后的字节长度; - 3.2 将
data转成buffer, 首先调用JSON.stringify后序列化data,在序列化的过程中会 data 中的正则交由JSON.stirngify的第二个参数replacer处理一下(JSON.stringify会对每个key调用replacer,用replacer函数的返回值替代原值) - 3.3 将
data被序列化后的长度写入lengthBuffer; - 3.4 将
lengthBuffer和 序列化后的data的buffer数据写入到wripePipe;
- 3.1 准备四个字节的
class PoolWorker {
writeJson(data) {
// 分配 4 个字节的 buffer用来盛放数据长度
const lengthBuffer = Buffer.alloc(4);
// 序列化 data,并转成 buffer
const messageBuffer = Buffer.from(JSON.stringify(data, replacer), 'utf-8');
// 把 data 长度写入 lengthBuffer
lengthBuffer.writeInt32BE(messageBuffer.length, 0);
// data 长度 buffer 写入 writePipe
this.writePipe.write(lengthBuffer);
// 数据 buffer 写入 writePipe
this.writePipe.write(messageBuffer);
}
}
4.1 JSON.stringify
JSON.stringify(value[, replacer [, space]])
JSON.stringify 常用的情况是传递一个带序列化的对象或者数组,但是他一共有三个参数,后两个可选:
value:待序列化的对象或者数组;replacer:可选的,替换函数;- 2.1 如果该选项为一个函数,则对
value中的每个key对应的值调用该replacer,replacer返回值则会替换掉原有的值被序列化,这个过程是递归的; - 2.2 如果该选项为一个数组,则只有包含在这个数组的
key会被序列化到最后的结果中;
- 2.1 如果该选项为一个函数,则对
space:空格,指定缩进用的空白字符串,用于美化输出(pretty-print);
4.2 replacer 保留正则信息
之所以要这么做,是因为正则无法被 JSON.stringify 序列化,后果是丢失正则信息;
-
方法位置:
thread-loader/src/serializer.js -> function replacer -
方法参数:
key,value;JSON.stringify调用replacer时传入的对象的key和value -
方法作用:在
thread-loader中,这个replacer是在writeJson前序列化对象时被JSON.stringify调用,核心作用是以对象的形式留存对象中的正则信息;
export function replacer(_key, value) {
if (value instanceof RegExp) {
// 将正则的描述信息以对象的形式留存,防止丢失
// 将来在使用的时候再根据这些信息还原成真实正则
return {
__serialized_type: 'RegExp',
source: value.source, // 正则表达式字符串
flags: value.flags, // 正则修饰符
};
}
return value;
}
五、总结
本文梳理了一遍从开始到 PoolWorker.prototype.run 方法的调用链路,然后分析得到 PoolWorker 实例后调用实例的 run 方法向 writePipe 写入数据的过程;
在这个过程中,通过 jobId 缓存本次 run 方法接收的 data 和 calback,这个 callback 是 neo-async 的 done 方法,data 则是包含 loader 的对象,这个 data 是要交给子进程去执行 loader 用的;
然后分析 writeJson 的逻辑,向 writePipe 写入序列化的 data buffer 数据以及 data 的长度数据;
好了,到这里我们已经把数据写到 writePipe 了,这也标志着把任务数据告知了子进程。与此同时主进程中前半部分说完了,包含初始化管控 worker 进程的 WorkerPool 类实例、负责具体处理通信和创建子进程的 PoolWorker 类,这期间还穿插了 neo-async 和进程间通信的知识。
从下一篇开始我们正式进入子进程的逻辑~~~