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

209 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的14天,点击查看活动详情

码字不易,喜欢点赞,不喜请移步评论区告知,未经授权不得转载

一、前情回顾

上一篇小作文详细讨论 neo-async/mapSeries 方法的原理:

  1. 接收 collection 对象,可以是对象、数组或者其他部署了迭代器接口的对象;

  2. collection 类型为对象或者数组时,这个时候根据 iterator.length 重载 iterator,即iterateor 是否需要对象的 key(或数组索引)

  3. 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

  1. 方法位置:thread-loader/src/WorkerPool.js -> class PoolWorker -> run

  2. 方法参数:

    • 2.1 data:数据对象,要传递给子进程处理的信息。这 data 就是我们的老相识 data 对象,是在 thread-loaderpitch 方法中调用 workerPool.run(data, cb) 传入的,这个 data 类似 webpackloaderContext 对象,包含 loaders 属性,这个 loaders 的值是除了 thread-loaders 以外的 loader。除 loaders 属性还有 resolvegetResolveloadModuele 等方法
    • 2.2 callback:回调函数,run 执行结束后要执行的方法。上面的 data 参数虽然是最初的 data 对象,但是这里 callback 却不是 cbcallbackneo-async 中的 runQueue 传入的私有方法 done,是处 neo-async 的并发逻辑的。
  3. 方法作用:

    • 3.1 维护 nextJobId 累加,缓存上一次的 nextJobId 作为本次 job 的的 id,以 jobIdkey,将本次 run 方法接收的 datacallback 组成的对象缓存到 this.jobs
    • 3.2 通过 this.writeJson 方法将本次 job 写入用于父子进程通信的 writePipe 管道,被写入的值是个对象,包含 type/id/data 三个属性。通过 writeJson 名字可以看出,并没有直接把对象传给 writePipe,而是传递的序列化的 json 对象;
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

  1. 方法位置:thread-loader/src/WorkerPool.js -> class PoolWorker -> writeJson

  2. 方法参数:data,需要通过 writePipe 写入给子进程的数据,这里面包含的是需要给子进程运行的 loader 及运行 loader 需要的一些数据、配置等

  3. 方法作用:

    • 3.1 准备四个字节的 bufferlengthBuffer,这个 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 和 序列化后的 databuffer 数据写入到 wripePipe
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 常用的情况是传递一个带序列化的对象或者数组,但是他一共有三个参数,后两个可选:

  1. value:待序列化的对象或者数组;
  2. replacer:可选的,替换函数;
    • 2.1 如果该选项为一个函数,则对 value 中的每个 key 对应的值调用该 replacerreplacer 返回值则会替换掉原有的值被序列化,这个过程是递归的;
    • 2.2 如果该选项为一个数组,则只有包含在这个数组的 key 会被序列化到最后的结果中;
  3. space:空格,指定缩进用的空白字符串,用于美化输出(pretty-print);

image.png

4.2 replacer 保留正则信息

之所以要这么做,是因为正则无法被 JSON.stringify 序列化,后果是丢失正则信息;

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

  2. 方法参数:key,valueJSON.stringify 调用 replacer 时传入的对象的 keyvalue

  3. 方法作用:在 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 方法接收的 datacalback,这个 callbackneo-asyncdone 方法,data 则是包含 loader 的对象,这个 data 是要交给子进程去执行 loader 用的;

然后分析 writeJson 的逻辑,向 writePipe 写入序列化的 data buffer 数据以及 data 的长度数据;

好了,到这里我们已经把数据写到 writePipe 了,这也标志着把任务数据告知了子进程。与此同时主进程中前半部分说完了,包含初始化管控 worker 进程的 WorkerPool 类实例、负责具体处理通信和创建子进程的 PoolWorker 类,这期间还穿插了 neo-async 和进程间通信的知识。

从下一篇开始我们正式进入子进程的逻辑~~~