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

236 阅读6分钟

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

一、前情回顾

上一篇小作文文介绍 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 对象、添加依赖(addDependency)的 cb 回调;

本篇的主题则是看看 workerPool.run(data, cb) 方法都做了哪些工作,该方法来自 WorkerPool.prototype.run

二、WorkerPoll.prototype.run

  1. 方法位置:src/WorkerPool.js
  2. 方法参数:
    • data: 要加入到 poolQueue(即上文 neo-async/queue.js 创建的队列)中的数据,下称 data
    • callback:poolQueue 的 worker 方法执行结束后要调用的回调方法,下称 cb。这个 cb 是接收 thread-loader 运行 loader 后的结果用的。
 data = { 
   loaders: this.loader.slice(1, ....), // 除 thread-loader 之外的其他loader
   resource: ..., 
   query: ...,
   sourceMap: ...,
   resourceQuery: ...
   // ....
 }
 // cb
 callback = (r) => { 
   // callback 是接收 thread-loader 并发运行 loader 后的结果用的
   // 结果包含了不同类型的依赖,然后调用相应类型的添加依赖的方法添加依赖
   if (r) {
     f.fileDependency.forEach(d => this.addDependency(d));.... 
   }
 }
  1. 方法作用:
    • 3.1 维护 this.activeJobs 累加
    • 3.2 向 workerPool.poolQueuepush 数据
WorkerPool {
  constructor(options) {
    // ...
    this.poolQueue = asyncQueue(
      this.distributeJob.bind (this), // poolQueue 的 worker 方法
      options.poolParallelJobs // 并发数
    );
   // ....
  }

  distributeJob(data, callback) {}
  
  // 这个 run 就是主角了 
  run(data, callback) {
    if (this.timeout) {
      clearTimeout(this.timeout);
      this.timeout = null;
    }
    this.activeJobs += 1;
    
    this.poolQueue.push(data, callback);
  }
}

三、this.poolQueue.push 方法

上文中提到过 this.poolQueueneo-async/queue.js 创建出来的,它是一个双向链表,并且实现了 push、unshift、shift 等操作方法。

neo-async/queue.js 创建出来的队列是个需要异步管控的队列,这个异步队列创建时接收处理对列中 dataworker(还接收一个最大并发数量的 concurency )。

当向异步队列(this.poolQueue)添加数据即 this.poolQueue.push 时,传递了 data 和一个 callback 函数,这个 callback 是最终接收 worker 处理 data 后的结果用的。

3.1 baseQueue 中的私有 push 方法

  1. 方法位置:node_modules/neo-async/async.js -> function baseQueue -> q.push

  2. 方法参数:

    • 2.1 tasks: 从上面的 workerPool.run 的调用能够看出,tasks 就是包含 loadersdata 对象
    • 2.2 callback: 这个 callback 就是前面的 cb,作用是接收 worker 处理 data 后返回的依赖。
  3. 方法作用:push 方法是 _insert 的科里化方法,为 _insert 的第三个参数绑定 undefined(不给它传第三个参数就是绑定 undefined

// queue 就是 baseQueue 的科里化方法
function queue(worker, concurrency) {
  return baseQueue(true, worker, concurrency);
}

// baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {

  var _callback, _unshift;

  // 上面的 this.poolQueue 就是这个 q 对象
  var q = {
 
    push: push, // this.poolQueue.push 就是下面的 function push
    
    process: isQueue ? runQueue : runCargo, 
    
    _worker: worker 
  };
  
  return q;
  
  // push 方法在这里
  function push(tasks, callback) {
    // 只给 _insert 传两个参数
    // 而 _insert 本身是接收三个参数的
    _insert(tasks, callback);
  }

  function _exec(task) { }

  function _insert(tasks, callback, unshift) {}

  function _next(q, tasks) { }

  function runQueue() { }
}

3.2 baseQueue 私有方法 _insert

  1. 方法位置:node_modules/neo-async/async.js -> function baseQueue -> function _insert

  2. 方法参数:

    • 2.1 tasks: 要添加到 poolQueue 的数据,也是需要被 worker 处理的任务,所以叫做 tasks;它接收到就是上面的 data 对象
    • 2.2 callback:回调函数,这个回调是接收 tasks 被处理后的结果的。在这里,就是前面的 cb 函数了。
    • 2.3 unshift:向队列开头插入方法,这里是 undefined
  3. 方法作用:

    • 3.1 格式化 tasks 参数,如果不是数组,将其包装成数组 最后赋值到 _tasks 变量
    • 3.2 将 callbackunshift 参数分别赋值到 _callback_unshift 变量
    • 3.3 调用 arrayEachSync 方法遍历 _tasks 数组,这里面放的就是前面的 data(这里有 loaders) 对象,遍历时,调用的迭代方法是 _exec 方法
// queue 就是 baseQueue 的科里化方法
function queue(worker, concurrency) {
  return baseQueue(true, worker, concurrency);
}

// baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {
  var _callback, _unshift;

  // 上面的 this.poolQueue 就是这个 q 对象
  var q = {
    push: push, // this.poolQueue.push 就是下面的 function push
  };
  
  return q;
  
 
  function push(tasks, callback) {
    // _insert 是下面的 function _insert 
    _insert(tasks, callback);
  }

  function _insert(tasks, callback, unshift) {
    // tasks 就是 data 对象: { loaders, resource, query ... }
    // callback 就是上面的收集依赖的 cb 函数: (r) => { if (r) this.fileDependencies.forEach(d =>  this.addDependency(d)) }
  
    q.started = true;
    var _tasks = isArray(tasks) ? tasks : [tasks];

    if (tasks === undefined || !_tasks.length) {
      if (q.idle()) {
        nextTick(q.drain);
      }
      return;
    }

    // push 科里化了 _insert,此时 unshift 参数是 undefined
    _unshift = unshift;
    
    // 把收集依赖的 callback 函数赋值到 _callback 遍历,注意有一个下划线
    _callback = callback;

    // _tasks -> [{ loaders, source, query }]
    // _callback = (r) => ....this.addDependency(d)
    
    // arrayEachSync: 遍历 _tasks 数组,其迭代函数时 _exec 方法
    // 为 _tasks 每一项调用 _exec, 最后返回 _tasks
    arrayEachSync(_tasks, _exec);
    
    // 防止泄漏 _callback,要清除掉
    _callback = undefined;
  }
}

3.3 arrayEachSync 方法

  1. 方法位置:node_modules/neo-async/async.js -> function arrayEachSync

  2. 方法参数:

    • 2.1 array: 需要被遍历的数组;上面 _insert 调用时传入的是 _tasks 数组,数组项是 data 对象;
    • 2.2 iterator:遍历时为数组每一项执行的迭代函数;上面 _insert 调用时传入的是 _exec 方法;
  3. 方法作用:遍历 array 数组,调用 iterator 最后返回 array。有点像 Array.prototype.forEach,但是 forEach 没有返回值而已;

function arrayEachSync(array, iterator) {
  var index = -1;
  var size = array.length;

  while (++index < size) {
    iterator(array[index], index);
  }
  return array;
}

3.3 baseQueue 私有方法 _exec

  1. 方法位置:node_modules/neo-async/async.js -> function baseQueue -> function _exec

  2. 方法参数:task,需要被处理的任务,在 thread-loader 中是一个 data 对象(包含 loaders)

  3. 方法作用:

    • 3.1 将 push 接收到的 data(被保证成 _tasks 数组) 和 cb 回调(赋值到 _callback)格式化成统一的 item 对象,并加入到队列中;、
    • 3.2 在下个事件循环中调用 q.process 方法消耗队列。所谓消耗队列就是调用前面创建队列时传入的 worker处理队列中的对象(此时队列中的项形如{ data: task, callback: _callback },其中 task是包含loadersdata 对象,_callback就是接收worker处理task后的结果的cb` 回调)
function _exec(task) {
  var item = {
    data: task,
    callback: _callback
  };
  if (_unshift) {
    q._tasks.unshift(item);
  } else {
    q._tasks.push(item);
  }
  nextTick(q.process);
}

3.4 q.process 方法

  1. 方法位置:node_modules/neo-async/async.js -> function baseQueue -> q.process

  2. 方法参数:暂无

  3. 方法作用:根据 baseQueue 的第一个参数是否为 true,决定 q.process 表示不同的方法,这里 isQueuetrue,所以 q.process 代表的是 runQueue 方法,runQueue 方法是消耗队列的。

// queue 就是 baseQueue 的科里化方法,
// 绑定 baseQueue 的第一个参数 isQueue 为 true
function queue(worker, concurrency) {
  return baseQueue(true, worker, concurrency);
}

// baseQueue
function baseQueue(isQueue, worker, concurrency, payload) {

  var q = {
    process: isQueue ? runQueue : runCargo,  // isQueue 是 queue 科里化绑定好的 true
    _worker: worker // worker 就是创建 poolQueue 传入的 this.distributeJob 方法
  };
  
  return q;
  
  // isQueue 为 true,q.process 就是下面的 runQueue
  function runQueue() { }
}

3.5 baseQueue 私有方法 runQueue

  1. 方法位置:node_modules/neo-async/async.js -> function baseQueue -> function runQueue

  2. 方法参数:暂无

  3. 方法作用:这个 runQueue 就是实现 thread-loader 并发的核心了,这里进行了并发控制;

    • 3.1 结合当前队列的状态 q.paused/并发数限制/队列不为空 等条件,条件成立则依次出队列(shift)取出任务 task
    • 3.2 创建上一个 worker 结束后需要调用的 done 回调;
    • 3.3 将从队列中取出的调用创建队列时传入的 worker 方法,在 thread-loaderworkerPoolpoolQueue 中,worker 就是 this.distribute.bind(this)WorkerPool.prototype.distribute 方法;
function runQueue() {
  while (!q.paused && workers < q.concurrency && q._tasks.length) {
    var task = q._tasks.shift();
    workers++;
    workersList.push(task);
    if (q._tasks.length === 0) {
      q.empty();
    }
    if (workers === q.concurrency) {
      q.saturated();
    }
    
    // 这个是传递给 worker
    var done = _next(q, [task]);
    
    // worker 就是 this.distribute.bind(this) 即 WorkerPool.prototype.distribute 方法
    // 要记住 worker 收到的回调不是 cb,而是由 runQueue 创造出来的 done 方法
    worker(task.data, done);
  }
}

四、总结

本篇小作文介绍了 thread-loader 的入口方法 WorkerPool.prototype.run 方法的内部逻辑,重点介绍了 this.poolQueue.push 方法从接收数据(data, cb),一直到最后 neo-async 调用声明队列时传入的 worker 启动对 data 的处理的全过程。

这个部分涉及到 neo-asyncqueue 中的逻辑,这个逻辑是控制并发的核心,最后要强调几点:

  1. workerPool.run(data, cb) 传递的第一个参数包含除 thread-loader 以外的 loaders,我们称之为 data 对象,第二个参树是个回调,用于接收 worker 处理 data 后返回的 loaders 的执行结果的回调函数,称为 cb

  2. workerPool 是调用 asyncQueue 方法得到的异步队列,它的 workerthis.distribute 方法;

  3. neo-async 最后调用 worker 时,在 poolQueueworkerthis.distriburte,它接收的第二个参数不是前面的 cb,而是 runQueue 创造出来的 done 方法