多进程打包:thread-loader 源码(11)

182 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的16天,点击查看活动详情 码字不易,感谢阅读,你的点赞是我更文最大的动力,如有误请移步评论区告知,未经授权不得转载!

一、前情回顾

上文开始了子进程 worker.js 的代码详解,主要讨论了以下内容:

  1. 父进程创建子进程的过程分析,包含:
    • 1.1 解析 node 路径;
    • 1.2 分析 thread-loader 下的 worker.js 路径;
    • 1.3 传递并发数配置;
    • 1.3 设置 stdio:[, , , pipe, pipe] 自定义管道(最后这两个pipe)的方式处理进程间通信;
  2. 分析了 worker.js 的文件结构,期间分析了自定义管道通信在子进程中的创建 fd3/4 的流,此外还从代码中得出 readNextMessage 这个入口方法的声明和最后的调用;

本篇我们真是进入到 readNextMessage 方法的细节,前方高能,这是一个系列,如果你前面没读完,建议你反复的读上几遍,说句实在的,写这些文章时,这些代码已经读了很多遍,即便如此也不敢说自己已经通透了,只能说懂了而已。

最近看气球哥再见谭sir的视频,发现我身上有他的影子,从他身上看到了我以为自己没有的东西——孤独。致敬在外漂泊的朋友们,共勉!

二、readNextMessage 细节

  1. 方法位置:thread-loader/src/worker.js

  2. 方法参数:暂无

  3. 方法作用:从 readPipe 中先读取数据长度,得到数据长度后再读取数据,读取数据完成后交由 onMessasge 方法处理;

2.1 worker.js VS PoolWorker 的 readNextMessage

一路读着这个系列过来的朋友可以能记得在 PoolWorker 类的原型上也有一个 readNextMessage 方法,PoolWorker.prototype.readNextMessage

是的,这两个方法是异曲同工的,原理相似,都是从可读流中先读取长度,再得到数据长度之后再次从缓存中读取数据,接着交给处理数据的方法处理。

PoolWorkerreadNextMessage父进程读取子进程的写入进程处理,而 worker.jsreadNextMessage子进程读取父进程的写入进行对应的处理。

2.2 readNextMessage 代码实现

老规矩,我们先把代码简化一下,简化的方法也很简单,把所有处理异常的逻辑剔除掉,剩下的就是处理正常逻辑的代码了,细节如下:

  1. 调用 readBuffer 方法从 readPipe 上读取 4 个字节的 buffer,这 4 个字节存储的是后面数据的长度,这个设计在前面 PoolWorkerreadMessage 详述 readBuffer 的时候说过。缓存里的的结构类似 [长度:数据][长度:数据][长度:数据],要想得到数据,先读取到长度,接着在读取这个长度的字节数,就得到数据 buffer

  2. readBuffer 是个异步行为,需要将得到长度后再读取的行为放到一个回调函数中,得到数据后将 buffer 变成字符串,再调用 JSON.parse 解析成对象,解析过程中传入 reviver 函数,这个 reviver 和父进程的 replacer 相对应,是将正则描述对象重新实例化成正则对象。

  3. 将解析过的数据交由 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.stringifyreplacer 相对应,JSON.parse 还接收一个 reviver —— 转化器函数。

reviver 转换器, 如果传入该参数(函数),可以用来修改解析生成的原始值,调用时机在 parse 函数返回之前。

语法如下:

JSON.parse(text[, reviver])

2.3.1 为什么有这个东西?

是因为父进程调用 writeJsonwritePipe 中写入数据时,因为 RegExp 实例无法被 JSON.stringify 序列化而丢失,所以我们的作者想到了一个鸡贼的做法——序列化时通过 replacer 保存正则的描述信息,parse 时再通过正则描述信息重新还原成正则实例。

正则描述信息:

let desc =  {
  __serialized_type: 'RegExp',
  source: value.source,
  flags: value.flags,
};

2.3.2 reviver 方法

  1. 方法位置:thread-loader/src/serializer.js

  2. 方法参数:keyvalue 均为 JSON.parse 调用时传入的待转换 JSON 字符串中的属性和对应的值,这个方法的返回值将会替换源 value 为该 key 对应的值。

  3. 方法作用:将描述信息还原成正则

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) 中讨论过,这里不再展开详细讨论,简单回顾一下这个方法的作用:

  1. 判断如果 length0,则直接分配一个 0 字节的 buffer,然后调用 callback,因为不需要读取;

  2. 调用 readChunk 私有方法,这个方法内部实现就是给 pipe 绑定 data 事件,给可读流绑定 data 事件会触发缓存中的数据转移出来,这样就能接收到缓存区的数据了,把接收到的数据交给 onChunk 回调处理

  3. 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 方法的结构和大部分实现逻辑,具体如下:

  1. 讨论 PoolWorker.prototyoe.readNextMessage 和 这里的 readNextMessage 的区别和联系;

  2. readNextMessage 都是调用 readBuffer 先读长度,再读长度对应的数据 buffer,得到数据后交给对应的方法处理.;

  3. worker.js 处理数据的方法是 onMessage,这个方法下一篇详细讨论