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

1,007 阅读7分钟

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

一、前情回顾

从上一篇小作文开启了一个新坑,上文主要讨论了以下几个问题:

  1. thread-loader 作用;
  2. thread-loader 的入口文件;
  3. 复习 loader.pitch & normal 以及两者的顺序;
  4. thread-loader 利用 pitch 截取后面的 loader 扔到线程池;

本篇我们开始讨论 thread-loader 的内部实现细节,期间会涉及到相当多的准备知识,没有这些准备知识又会导致看不懂,不得已在讲代码的过程中会多次穿插,请看官老爷见谅,后期有可能会重新组织行文顺序。

二、src/index.js

// loader-utils 是个 webpack 用的工具库,包含很多的 loader 工具
import loaderUtils from 'loader-utils';

import { getPool } from './workerPools';

function pitch() {
  
}

function warmup(options, requires) {

}

export { pitch, warmup }; // eslint-disable-line import/prefer-default-export

三、src/index.js function pitch

function pitch() {
  // 获取 loader 的配置选项 
  const options = loaderUtils.getOptions(this);
  
  // 传入选项获取 workerPool
  const workerPool = getPool(options);

  if (!workerPool.isAbleToRun()) {
    return;
  }

  // 调用 this.async 将当前 loader 变成一个 异步 loader
  const callback = this.async();

  // 调用 workerPool.run 执行 thead-loader 后面的 loader
  // this.loaderIndex + 1 就是不包含 thread-loader
  workerPool.run(
    { // 这个对象称为 data
      // loaders 是除了 thread-loader 以外的 loader
      loaders: this.loaders.slice(this.loaderIndex + 1).map((l) => {
        return {
          loader: l.path, options: l.options, ident: l.ident,
        };
      }),
      // ...
    },
    (err, r) => {
      // 这个回调称为 cb
      // ... 
    }
  );
}

前面说过 pitch 的作用,是作为 thread-loaderpitching 阶段调用的方法,用于截取 thread-loader 后面的 loader 并把它们放到 worker pool 中,接着看看这个方法的具体实现;

  1. 使用 loaderUtils 获取在 webpack.config.js 中设置该 loader 时传入的参数,注意 loader 中的 thisloaderContext 对象,例如下面例子中的 { something: 'something' } 这个对象;
module.exports = {
  entry: './src/index.js',
  mode: 'development',
  // ...
  module: {
    rules: [
      {
        test: /.js$/,
        exclude: /node_modules/,
        use: [
          {
            loader: 'thread-loader',
            // 这个对象就是 loaderUtils.getOptions(this) 得到的对象
            options: { 
              something: 'something'
            }
          }
         // ....
        ]
      }
    ]
  },
  // ...
}
  1. 调用 this.async 方法将该 loader 变成一个异步的 loaderthis.async 返回的 callback 会在异步处理完成后调用,以继续调用后面的 loaderthis.loaderIndex++

  2. 调用 workerPool.run 方法,传入一个对象和会回调(对象我们称之为data,回调暂且叫做 cb);workerPool.run 结束后调用的方法;

    • 3.1 workerPoo.run 的 data 对象,这个对象有点像 loader 被调用时接收到 loaderContext 对象
        {
          loaders: ..., // 除 thread-loader 以外的 loader
          resource: this.resourcePath + (this.resourceQuery || ''), // 当前模块的资源路径
          sourceMap: this.sourceMap,
          emitError: this.emitError,
          emitWarning: this.emitWarning,
          loadModule: this.loadModule,
          resolve: this.resolve,
          getResolve: this.getResolve,
          target: this.target,
          minimize: this.minimize,
          resourceQuery: this.resourceQuery, // request 后面的 query
          optionsContext: this.rootContext || this.options.context, // 项目的根目录
          rootContext: this.rootContext, // 同样是项目根目录,也是 webpack 的 context 目录
        },
    
    • 3.2 workerPool.run 的 cb 回调函数
    (err, r) => {
      // 这个 r 是 run 方法的返回值,
      // r.result 是模块的代码,r.xxxDependencies 是该依赖的各类型的子依赖
      // cb 方法的作用就是把得到的 r 中的各种类型的子依赖添加到该模块对应类型的依赖下
      // 例如在我们的例子中就是 fileDependencies 文件依赖
      if (r) {
        r.fileDependencies.forEach(d => this.addDependency(d));
        r.contextDependencies.forEach(d => this.addContextDependency(d));
        r.missingDependencies.forEach(d => this.addMissingDependency(d));
        r.buildDependencies.forEach(d => // Compatibility with webpack v4
        this.addBuildDependency ? this.addBuildDependency(d) : this.addDependency(d));
      }
    
      if (err) {
        callback(err);
        return;
      }
      // callback 是 this.async 的返回值,用于告知 webpack 这个 loader 跑完了,
      // 进行后续的 loader 调用或者后续的打包工作
      callback(null, ...r.result);
    }
    
  • workerPool.run()cb 接收到的结果 r image.png

四、 getPool 和 workerPool 实例

workerPool 是在 thread-loaderpitch 方法中调用 getPool 方法得到的 WorkerPool 类的实例;先解释个名词 workerPool,字面意思,”worker池(工作进程池)“,也就是说这个 workerPool 是保存和调度“worker” 的。后面还有一个 poolWorker,这个名词就是池子中的 worker,是干活儿的 worker

4.1 pitch 方法调用 getPool

  • 方法位置:src/index.js
function pitch() {
  // getPool 创建或者获取当前 options 对应的 workerPool
  const workerPool = getPool(options);
}  

4.2 getPool 方法

  • 方法位置:src/workerPools.js
  • 方法参数:thread-loaderwebpack.config.js 中的配置信息
  • 方法作用:
      1. 根据接收到的 options 选项格式化 workerPoolOptions 选项对象,这个对象中包含新建 worker 进程和调度worker池子的数据,如 numberOfWorkersworkerNodeArgspoolTimeout...
      1. 将上述得到的 workerPoolOptions 转成字符串,这个字符串将作为缓存子进程的 key
      1. 尝试从 workerPools 缓存对象中读取缓存,如果没有缓存就新建 WorkerPool 的实例并缓存
function getPool(options) {
  const workerPoolOptions = {
    name: options.name || '',
    numberOfWorkers: options.workers || calculateNumberOfWorkers(),
    workerNodeArgs: options.workerNodeArgs,
    workerParallelJobs: options.workerParallelJobs || 20,
    poolTimeout: options.poolTimeout || 500,
    poolParallelJobs: options.poolParallelJobs || 200,
    poolRespawn: options.poolRespawn || false,
  };
  
  // 选项对象作为 key
  const tpKey = JSON.stringify(workerPoolOptions);

  // 根据 workerPoolOptions 缓存已经创建的 worker
  workerPools[tpKey] = workerPools[tpKey] || new WorkerPool(workerPoolOptions);

  // 返回 WorkerPool 实例
  return workerPools[tpKey];
}

五、 WokerPool 类

export default class WorkerPool {
  constructor(options) {
    this.options = options || {};
    this.numberOfWorkers = options.numberOfWorkers; // 最大工作进程数
    this.poolTimeout = options.poolTimeout; // 进程闲置时长,超过后会 kill worker 进程
    this.workerNodeArgs = options.workerNodeArgs; // 传递给子进程的 process argv 参数
    this.workerParallelJobs = options.workerParallelJobs;
    this.workers = new Set();
    this.activeJobs = 0;
    this.timeout = null;
    
    // 调度 worker 的异步队列
    this.poolQueue = asyncQueue(
      this.distributeJob.bind (this), // 创建 worker 并分配任务给 worker 
      options.poolParallelJobs // 并发数
    );
    this.terminated = false;

    this.setupLifeCycle();
  }

  run(data, callback) {
    // ...
  }

  distributeJob(data, callback) {
    // ...
  }

  createWorker() {
    // ...
  }
}

5.1 asyncQueue

asyncQueue 来自另一个知名库 neo-async。这个东西用于创建一个异步队列,并且提供了向队列中插入、移除、遍历的方法,方便进行异步队列的管理。

import asyncQueue from 'neo-async/queue';

5.1.1 neo-async/queue.js

'use strict';

module.exports = require('./async').queue;

可以看出 neo-async/queue.js 中导出的 queueasync.js 导出对象上的一个属性

5.1.2 async.js

async.js 中导出的内容量十分巨大,这里我们只关注 queue

(function(global, factory) {
  /*jshint -W030 */
  'use strict';
  typeof exports === 'object' && typeof module !== 'undefined'
    ? factory(exports)
    : typeof define === 'function' && define.amd
    ? define(['exports'], factory)
    : global.async
    ? factory((global.neo_async = global.neo_async || {}))
    : factory((global.async = global.async || {}));
})(this, function(exports) {
  
  // 这个 index 是最终导出对象
  var index = {
    queue: queue
  };
  
  // 导出 index
  export['default'] = index;
  
  // queue 的实现函数
  function queue(worker, concurrency) {  }
})

5.1.3 async.js -> function queque

function queue(worker, concurrency) {
  return baseQueue(true, worker, concurrency);
}

async.js 导出的 queuebaseQueue 的一个科里化函数,所以真正的实现逻辑还要往下看 baseQueue 方法

5.1.4 async.js -> function baseQueue

function baseQueue(isQueue, worker, concurrency, payload) {
  if (concurrency === undefined) {
    concurrency = 1;
  } else if (isNaN(concurrency) || concurrency < 1) {
    throw new Error('Concurrency must not be zero');
  }

  var workers = 0;
  var workersList = [];
  var _callback, _unshift;

  var q = {
    // .... q 有很多属性,暂时忽略掉,更容易理解主线
    
    // DLL 是动态双向链表
    _tasks: new DLL(), 
    
     // 向链表中插入项
    push: push,
    
    // 队列的处理方法,isQueue 为 true,是上面 function queue 传入的 baseQueue(true, ....)
    process: isQueue ? runQueue : runCargo, 
    
    idle: idle, // 获取是否空闲,不重要
    
    // 这个 worker 就是前面 WokerPool 实例创建时传入的 this.distributeJob.bind(this)
    _worker: worker 
  };
  
  return q;

  function push(tasks, callback) {
    _insert(tasks, callback);
  }

  function _exec(task) { }

  function _insert(tasks, callback, unshift) {}

  function _next(q, tasks) { }

  function runQueue() { }
}

5.1.5 总结一下 asyncQueue

虽然还没有介绍 queue 的具体运行原理,但是这里需要先剧透介绍一下,咱们的小作文是按照代码执行顺序组织的,介绍一下 queue 的原理,后面理解起来就会轻松很多。

首先是 thread-loaderpitch 方法创建 WorkerPool 实例,而创建 WokerPool 实例执行构造函数时会在 WorkerPool 实例上初始化 queue 属性,this.queue = asyncQueue(worker, concurency)

asyncQueue 接收到 workerconcurency 后会初始化 q 并返回对象,q 包含以下几个重点属性和方法:

  1. _tasks:用双向链表实现的队列,其中用于存放待遍历的 data 数据;
  2. push:向双向链表添加 data 的的方法,push 后会启动消耗 _tasks 中的队列
  3. process:消耗队列的方法,在当前场景下对应的 runQueue 方法,这个方法中会调用上面 asyncQueue 收到的 worker 函数,并传给 _tasks 中的 data

有了这几个属性,就好理解 this.queue 的作用了:当后面调用 this.queue.push 时,首先把 data 加入到队列,接着启动对列的消耗,这个过程就是每次从队列开头 shift 一项,然后调用 worker 函数,并传递刚刚从队列 shift 下列的 data

而在 WorkerPool 中,worker 就是 this.distributeJob 这个方法,至于 push 动作则是在得到 WorkerPool 实例后,调用实例的 run 方法中发生的,也就前面 pitch 方法的最后一步 workerPool.run(data, cb)

六、总结

本文介绍了 thread-loaderpitch 方法实现的具体逻辑:

  1. 使用 loaderUtils 获取 thread-loaderwebpack.config.js 中的配置 options
  2. 调用 getPool 方法获取 WorkerPool 实例;这个过程中会用到 neo-asyncqueue,并且介绍了 queue 的作用和运行机制;
  3. 调用 this.async()thread-loader 变成一个异步 loader
  4. 调用 workerPool.run() 方法并传入包含 除 thread-loader 之外的其他 loaderdata 对象和添加依赖的 cb 回调;

下一篇讨论 workerPool.run 方法,这个方法是 thread-loader 的入口方法,这里就要用到上面花了大量篇幅说用的 asyncQueue