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

191 阅读6分钟

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

码字不易,感谢阅读,你的点赞让人振奋!
如文章有误请移步评论区告知,万分感谢!
未经授权不得转载!

一、前情回顾

上一篇小作文把 worker.js 实现的 loaderContext 上的各个方法挨个分析了一遍,这里并不包含父子进程通信的部分,主要包含以下方法:

  1. loaderContext.resolve 解析一个 request;
  2. loaderContext.getResolve 方法,获取一个解析 request 的函数,可以支持 callback 和 Promise;
  3. loaderContext.getOptions 获取 loader 的选项对象;
  4. loaderContext.emitWarning/emitError 抛出警告和错误信息,编译后输出到命令行;
  5. loaderContext.exec 执行代码片段;
  6. loaderContext.addBuildDependency 方法,收集 buildDependency;

到这里,runLoader 的第一个参数基本已经结束了,本文我们准备讨论 runLoaders 的结果回调,即 loader 运行结束之后的逻辑。

二、runLoaders 结果回调

runLoaders 的结果回调是调用 loaderRunner.runLoaders(options, callback) 时传递的第二个参数 callback,这个回调函数将会在 options.loaders 全部被调用后执行,并传入 loaders 运行的结果。

在 thread-loader 中,worker.js 去跑 loader,这个过程是在子进程中完成的,而 webpack 则运行在父进程,所以这个 runLoaders 的结果回到核心作用就是把结果加工并发送给父进程。

loaderRunner.runLoaders(
  { // runLoaders options 选项对象
    loaders: data.loaders,
    context: {
      resolve: () => {},
      loadModule: () => {}
      // ....
    }
  },
  (err, lrResult) => {
     // runLoaders 结果回调
     // lrResult 就是这些 loader 运行结束后的结果对象
  }
);

三、runLoaders 结果回到细节

  1. 从 lrResult 中解构 result/cachable/fileDependencies/contextDependencies/missingDeependencies;

  2. 通过 map 方法转换得到的 result 数组 convertedResult,这个转换后的数组是 result 的描述数组,例如如果数组项是 buffer,如果 result 中的每一项是 buffer 或者字符串的情况下,将其都转成 buffer 并标记为 buffer:true,如果是字符串还要表示 string: true。这些描述信息会在后面用于还原 result 数组;然后将 buffer 或者字符串转成的 buffer push 到 bufferToSend 这个待发送给父进程的数组中;

  3. 通过 writeJson 写入一个 type: job 的消息到通信管道,这个消息将会被父进程接收,进而被父进程的 onWorkerMessage 处理;

  4. 将 bufferToSend 中的每一段 buffer 都写入到 writePipe 管道中,然后就是等待父进程从管道中读取了,这个读取的动作是前面 3 中父进程收到 type: job 的消息后的逻辑了;

  5. 在下个事件循环(setImmediate)调用 taskCallback,这个 taskCallback 是 neo-async 这个库中的 runQueue 时传入的 done 方法,是控制并发用的;

loaderRunner.runLoaders(
  { 
    // runLoaders options 选项对象 
  },
  (err, lrResult) => {
   // 从 lrResult 中解构需要的的属性
    const {
      result,
      cacheable,
      fileDependencies,
      contextDependencies,
      missingDependencies,
    } = lrResult;
    
    // buffersToSend 是待发送的 buffer
    const buffersToSend = [];
    
    // convertedResult 如果 result 是个数组,进行转换
    const convertedResult =
      Array.isArray(result) &&
      result.map((item) => {
        const isBuffer = Buffer.isBuffer(item);
        
        // 数组项是 buffer
        if (isBuffer) {
          // push 到 buffersToSend 数组
          buffersToSend.push(item);
          return {
            buffer: true, // 标记当前项是 buffer
          };
        }
        
        // 数组项是字符串
        if (typeof item === 'string') {
          // 字符串转 buffer
          const stringBuffer = Buffer.from(item, 'utf-8');
          
          // push 到 buffersToSend 数组
          buffersToSend.push(stringBuffer);
          return {
            buffer: true, // 标记是 buffer
            string: true, // 同时也是字符串
          };
        }
        
        // 不是 buffer 也不是字符串,
        // 包装成对象
        return {
          data: item,
        };
      });
      
    // 向管道写入 type: job 的消息
    // 父进程读取到消息后触发 onWorkerMessage 方法
    writeJson({
      type: 'job',
      id,
      error: err && toErrorObj(err),
      result: {
        result: convertedResult, // 转换出来的结果
        cacheable,
        
        // 各种依赖
        fileDependencies,
        contextDependencies,
        missingDependencies,
        buildDependencies,
      },
      
      // data 中每一项是 buffersToSend 每一个 buffer 长度
      // 形如:[100, 48, 23, 1022] 
      // 这个有啥用?告知父进程每次读取 data 中的一项表示的 buffer 长度,
      // 就可以得到一个结果结果项
      data: buffersToSend.map((buffer) => buffer.length),
    });
    
    // 最后把 result 中能转成 buffer 的结果组成的数组逐项写入管道
    // 注意,buffersToSend 中的每一项内上面的长度是对应的
    buffersToSend.forEach((buffer) => {
      writePipeWrite(buffer);
    });
    setImmediate(taskCallback);
  }
);

四、父进程处理 type: job

我们目前的这个行文顺序是按照代码的执行顺序组织的,如果不读代码就是读一读文章,估计很难收到启发,强烈建议你读一读源码,一共就几个 js 文件,结合着这个系列的文章,你肯定能看懂的。我估计文章得字数应该和代码字数一比一了,等到源码结束了,我会重新梳理一遍,从宏观再次把握一遍 thread-loader 的架构设计。

上一个标题 runLoaders 的结果回调的最后通过 writeJson 向管道写入了 type: job 的消息,父进程就会读取到这个消息,进而触发父进程的 onWorkerMessage:

  1. 从 message 中获取 data,err,result 属性,通过上面的代码可以看出:
    • data: 表示 worker 写入到管道的 result 各项长度数组,比如 [100, 48, 23, 1022];
    • result: 是 worker 传递过来的的消息中表示 loader 运行结果的对象,形如:{ result: convertedResult, fileDependencies... }
  2. 异步串行读取遍历 data,data 中的每一项表示要从管道中读取该项表示的长度的 buffer,这一段 buffer 就是 loader 运行后的一个结果;调用 asyncMapSeries 方法,这个方法我们前面说过它,异步串行遍历,最后调用回调并传入各次迭代函数运行后的结果组成的集合,类似 Promise.all 方法;
    • 2.1 第一个参数是个待遍历的数组;
    • 2.2 第二个是迭代函数,接收两个参数,第一个是数组项,第二个是 callback。为数组中的每一项调用这个迭代函数,并传入该数组项;这个 callback 是 neo-async 内部自己提供的方法,用于向结果集中添加数据,并且添加顺序是有保证的;
    • 2.3 第三个是遍历结束后要执行的函数,接收前面的结果集合,这个集合是迭代函数中调用 callback 添加的,是个有顺序的结果;
// class PoolWorker 原型方法
onWorkerMessage(message, finalCallback) {
  const { type, id } = message;
  switch (type) {
    case 'job': {
      const { data, error, result } = message;
      asyncMapSeries(
        data,
        
        // this.readBuffer 会把读到的 buffer 传给 callback
        // callback 会收集 buffer 到内部的一个结果集合
        (length, callback) => this.readBuffer(length, callback),
        
        // data 中的每一项都被上面的第二个参数遍历后才会执行这个回调
        // buffers 就是结果集,
        // 这个步骤相当于还原子进程中遍历 buffersToSend 把每一段写入管道的过程
        // 把每一段再收集到一个数组中
        (eachErr, buffers) => {
          // 从 jobs 中读取到本次运行 loader 对应的回调函数
          const { callback: jobCallback } = this.jobs[id];
          
          // 定义 callback
          const callback = (err, arg) => {
            if (jobCallback) {
              // 从 this.jobs 中把本次任务对应的回调移除
              delete this.jobs[id];
              
              // 活跃任务数递减
              this.activeJobs -= 1;
              
              // 调用 onJobDone
              this.onJobDone();
              if (err) {
                // err handler
              } else {
                
                // 调用本次运行 loader 对应的回调
                // 这个 jobCallabck 是 distributeJob 传入的 done
                jobCallback(null, arg);
              }
            }
            finalCallback();
          };
          
          // 把 result 中的数据还原,
          // 是前面子进程转换 result 得到 convertedResult 的逆过程
          let bufferPosition = 0;
          if (result.result) {
            result.result = result.result.map((r) => {
              if (r.buffer) {
                const buffer = buffers[bufferPosition];
                bufferPosition += 1;
                if (r.string) {
                  return buffer.toString('utf-8');
                }
                return buffer;
              }
              return r.data;
            });
          }
          if (error) { // error handler }
          
          // 调用 callback
          callback(null, result);
        }
      );
      break;
    }
    
   // ... case: others
  }
}

五、总结

本篇小作文详细讨论了子进程 runLoaders 的结果回调的逻辑:

  1. 得到 runLoaders 的结果中的 result 和各种依赖数组;
  2. 转换 result 数组为 convertedResult,这个过程中会把 result 中的 buffer、字符串变成 buffer 并添加描述信息,这些描述信息用于父进程从管道中读取 buffer 并还原 result,result 中非 buffer 或字符串的原样保留;
  3. 向父进程发送 type: job 的消息,消息包含各段 buffer 长度的 data 数组、描述loader 运行 result 的描述信息 result(实为 convertedResult 数组),父进程则触发 onWorkerMessage 处理,处理过程利用 asyncMapSeries 遍历 buffer 长度数组拼接 buffer,最后还原 loader 的 result 结果并调用回 jobCallback 回调;