持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的16天,点击查看活动详情 码字不易,感谢阅读,你的点赞是我更文最大的动力,如有误请移步评论区告知,未经授权不得转载!
一、前情回顾
上文开始了子进程 worker.js 的代码详解,主要讨论了以下内容:
- 父进程创建子进程的过程分析,包含:
- 1.1 解析 node 路径;
- 1.2 分析
thread-loader下的worker.js路径; - 1.3 传递并发数配置;
- 1.3 设置
stdio:[, , , pipe, pipe]自定义管道(最后这两个pipe)的方式处理进程间通信;
- 分析了
worker.js的文件结构,期间分析了自定义管道通信在子进程中的创建fd为3/4的流,此外还从代码中得出readNextMessage这个入口方法的声明和最后的调用;
本篇我们真是进入到 readNextMessage 方法的细节,前方高能,这是一个系列,如果你前面没读完,建议你反复的读上几遍,说句实在的,写这些文章时,这些代码已经读了很多遍,即便如此也不敢说自己已经通透了,只能说懂了而已。
最近看气球哥再见谭sir的视频,发现我身上有他的影子,从他身上看到了我以为自己没有的东西——孤独。致敬在外漂泊的朋友们,共勉!
二、readNextMessage 细节
-
方法位置:
thread-loader/src/worker.js -
方法参数:暂无
-
方法作用:从
readPipe中先读取数据长度,得到数据长度后再读取数据,读取数据完成后交由onMessasge方法处理;
2.1 worker.js VS PoolWorker 的 readNextMessage
一路读着这个系列过来的朋友可以能记得在 PoolWorker 类的原型上也有一个 readNextMessage 方法,PoolWorker.prototype.readNextMessage。
是的,这两个方法是异曲同工的,原理相似,都是从可读流中先读取长度,再得到数据长度之后再次从缓存中读取数据,接着交给处理数据的方法处理。
PoolWorker 的 readNextMessage 是父进程读取子进程的写入进程处理,而 worker.js 的 readNextMessage 是子进程读取父进程的写入进行对应的处理。
2.2 readNextMessage 代码实现
老规矩,我们先把代码简化一下,简化的方法也很简单,把所有处理异常的逻辑剔除掉,剩下的就是处理正常逻辑的代码了,细节如下:
-
调用
readBuffer方法从readPipe上读取4个字节的buffer,这4个字节存储的是后面数据的长度,这个设计在前面PoolWorker的readMessage详述readBuffer的时候说过。缓存里的的结构类似[长度:数据][长度:数据][长度:数据],要想得到数据,先读取到长度,接着在读取这个长度的字节数,就得到数据buffer。 -
readBuffer是个异步行为,需要将得到长度后再读取的行为放到一个回调函数中,得到数据后将buffer变成字符串,再调用JSON.parse解析成对象,解析过程中传入reviver函数,这个reviver和父进程的replacer相对应,是将正则描述对象重新实例化成正则对象。 -
将解析过的数据交由
onMessage处理,并在下个tick继续调用readNextMessage进行读取;
function readNextMessage() {
// readBuffer 读取 4 字节的数据长度
readBuffer(readPipe, 4, (lengthReadError, lengthBuffer) => {
// 得到长度
const length = lengthBuffer.length && lengthBuffer.readInt32BE(0);
// readBuffer 读取 length 个字节就得到本次父进程传递过来的数据
readBuffer(readPipe, length, (messageError, messageBuffer) => {
// 将数据 buffer 先转成字符串
const messageString = messageBuffer.toString('utf-8');
// 字符串变成数据对象
const message = JSON.parse(messageString, reviver);
// 交给 onMessage 处理
onMessage(message);
// 下个 tick 接着读取 readNextMessage 后面的消息
setImmediate(() => readNextMessage());
});
});
}
2.3 JSON.parse & reviver
这个写法是 JSON.parse 的语法,一般情况下常用的就是简单粗暴的 JSON.parse。和前文的 JSON.stringify 的 replacer 相对应,JSON.parse 还接收一个 reviver —— 转化器函数。
reviver 转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。
语法如下:
JSON.parse(text[, reviver])
2.3.1 为什么有这个东西?
是因为父进程调用 writeJson 向 writePipe 中写入数据时,因为 RegExp 实例无法被 JSON.stringify 序列化而丢失,所以我们的作者想到了一个鸡贼的做法——序列化时通过 replacer 保存正则的描述信息,parse 时再通过正则描述信息重新还原成正则实例。
正则描述信息:
let desc = {
__serialized_type: 'RegExp',
source: value.source,
flags: value.flags,
};
2.3.2 reviver 方法
-
方法位置:thread-loader/src/serializer.js
-
方法参数:
key,value均为JSON.parse调用时传入的待转换JSON字符串中的属性和对应的值,这个方法的返回值将会替换源value为该key对应的值。 -
方法作用:将描述信息还原成正则
export function reviver(_key, value) {
if (typeof value === 'object' && value !== null) {
// eslint-disable-next-line no-underscore-dangle
if (value.__serialized_type === 'RegExp') {
return new RegExp(value.source, value.flags);
}
}
return value;
}
2.4 readBuffer 方法
这个方法在前面 多进程打包:thread-loader 源码(6) 中讨论过,这里不再展开详细讨论,简单回顾一下这个方法的作用:
-
判断如果
length为0,则直接分配一个0字节的buffer,然后调用callback,因为不需要读取; -
调用
readChunk私有方法,这个方法内部实现就是给pipe绑定data事件,给可读流绑定data事件会触发缓存中的数据转移出来,这样就能接收到缓存区的数据了,把接收到的数据交给onChunk回调处理 -
onChunk回调:从pipe缓存的buffer读取指定长度的内容,如果超出长度把超过的内容退回pipe的缓存区;
export default function readBuffer(pipe, length, callback) {
if (length === 0) {
callback(null, Buffer.alloc(0));
return;
}
let remainingLength = length;
const buffers = [];
const readChunk = () => {
const onChunk = () => { /* 具体读取 buffer 数据的逻辑 */ };
pipe.on('data', onChunk);
pipe.resume();
};
// 调用 readChunk
readChunk();
}
2.5 onMessage
该方法用于根据收到的父进程发来的数据并根据数据指示开展具体工作,这个方法下一篇继续讨论。
三、总结
本文详细讨论了 worker.js 中的 readNextMessage 方法的结构和大部分实现逻辑,具体如下:
-
讨论
PoolWorker.prototyoe.readNextMessage和 这里的readNextMessage的区别和联系; -
readNextMessage都是调用readBuffer先读长度,再读长度对应的数据buffer,得到数据后交给对应的方法处理.; -
worker.js处理数据的方法是onMessage,这个方法下一篇详细讨论