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

495 阅读5分钟

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

一、前情回顾

上一篇小作文文讨论 onWorkerMessage 的方法核心逻辑,并就 job 类型进行了展开讨论,其他类型做了简单说明:

本篇小作文我们要详细讨论在 onWokerMessage 处理 message.typejob 使用到的 neo-async/mapSeries 方法的核心实现,以及如何确保异步串行的结果的顺序保证。

二、 neo-async/mapSeries 方法

在上面的 onWorkerMessage 方法中,我们看到在 message.typejob 时调用 asyncMapSeries 方法进程异步的遍历。现在我们来说说这个方法,类似前面说 neo-async/queue 一样,说清楚这个方法会更容易立即 thread-loader 做的工作。

  1. 方法位置:node_modules/neo-async/mapSeries.js

  2. 方法参数:

    • 2.1 collection:待遍历的集合对象,是一个数组、对象或者其他部署了遍历器接口的对象;
    • 2.2 iterator:迭代器方法,为 collection 中的每一项调用一次这个 iterator 方法;
    • 2.3 callback:回调函数,当遍历完成后要执行的回调方法,接收迭代处理的结果;
  3. 方法作用:异步串行遍历 collection 对象,在 iterator 中调用 done 方法将异步结果添加到 result 数组中,注意这个添加是可以保证顺如的,最后完成遍历后调用 callback 并传入 result 数组;类似 Promise.all,无论异步任务完成的时机如何都可以保证结果的顺序。

function mapSeries(collection, iterator, callback) {
  // 确保 callback 是个函数
  
  callback = callback || noop;
  
  var size, key, keys, iter, item, result, iterate;
  // 标识符是否同步
  var sync = false;
  
  var completed = 0;
  
  // collection 是否为 数组
  if (isArray(collection)) {
    size = collection.length;
    iterate = iterator.length === 3 ? arrayIteratorWithIndex : arrayIterator;
  } else if (!collection) {
  } else if (iteratorSymbol && collection[iteratorSymbol]) {
    // 部署了迭代器接口的对象
    size = Infinity;
    result = [];
    iter = collection[iteratorSymbol]();
    iterate = iterator.length === 3 ? symbolIteratorWithKey : symbolIterator;
  } else if (typeof collection === obj) {
    // collection 是对象,在 thread-loader 中属于这种情况
    
    // keys 就是 Object.keys 的返回值
    keys = nativeKeys(collection);
    
    // 对象的 key 的数量
    size = keys.length;
    
    // 根据 传入 iterator 函数形参个数重载 iterator
    // 所谓形参个数是说遍历对象时 iterator 函数:
    // 如果接收两个参数,表示接收的是对象每个 key 的 val, 另一个是 done 方法
    // 如果接收三个:value,key 和 done 方法
    iterate = iterator.length === 3 ? objectIteratorWithKey : objectIterator;
  }
  if (!size) {
    return callback(null, []);
  }
  result = result || Array(size);
  iterate();

  function arrayIterator() {
    iterator(collection[completed], done);
  }

  function arrayIteratorWithIndex() {
    iterator(collection[completed], completed, done);
  }

  function symbolIterator() {
    item = iter.next();
    item.done ? callback(null, result) : iterator(item.value, done);
  }

  function symbolIteratorWithKey() {
    item = iter.next();
    item.done ? callback(null, result) : iterator(item.value, completed, done);
  }
  // 对象 iterator 接收两个参数:value,done
  function objectIterator() {
    // completed 是个索引,从 Object.keys(collections) 返回的数组中取值的
    iterator(collection[keys[completed]], done);
  }
  
  // iterator 接收三个参数:val,key,done
  function objectIteratorWithKey() {
    key = keys[completed];
    iterator(collection[key], key, done);
  }

  // done 方法,实现异步串行的核心
  // 在 iterator 中调用的,调用 done 表示这个异步迭代工作已完成
  // err 表示错误,res 表示要存入 result 的结果
  function done(err, res) {
    // 向 result 中添加结果
    // 因为 completed 是个索引,
    // 所以异步迭代器的完成顺序就是 result 的结果顺序
    result[completed] = res;
    
    // 累加 completed 索引并判断是否已经完成
    // size 就是 Object.keys(collections).length
    if (++completed === size) {
      // 走道这里说明都完成了
      iterate = throwError;
      
      // 调用最终的回调并传入 result
      callback(null, result);
      callback = throwError;
    } else if (sync) {
      nextTick(iterate);
    } else {
      sync = true;
      iterate();
    }
    sync = false;
  }
}

三、neo-async/mapSeries 例子

上面说了这么多,感觉还是云里雾里的,看个例子,这个例子也是 neo-async 这个库里给这个 mapSeries 方法写的例子:


// 异步串行遍历数组
var order = [];
var array = [1, 3, 2];
var iterator = function(num, index, done) {
     setTimeout(function() {
     order.push([num, index]);
     done(null, num);
   }, num * 10);
 };
 async.mapSeries(array, iterator, function(err, res) {
   console.log(res); // [1, 3, 2]
   console.log(order); // [[1, 0], [3, 1], [2, 2]]
 });

// 异步串行遍历 object
var order = [];
var object = { a: 1, b: 3, c: 2 };
var iterator = function(num, done) {
  setTimeout(function() {
    order.push(num);
    // done 是保证顺序的核心
    done(null, num);
  }, num * 10);
};

async.mapSeries(object, iterator, function(err, res) {
  console.log(res); // [1, 3, 2]
  console.log(order); // [1, 3, 2]
});

在 iterator 中干了一件事,开启一个定时器延时分别为 10ms30ms20ms;按照大家的概念,执行顺序是 102030 的,但是你会发现,他的答案却不是这样的,而是 103020

他的诀窍在定时器中调用的 done 方法,从上面的源码中可以看出来,done 是上一个异步完成后才会调用,而 done 的调用会给保证 result 顺序的 completed 索引累加并且执行下一次迭代。

什么意思呢?也就是说103020 这三个定时器不是一次性开启的,如果一次性开启三个定时器,那肯定按照事件循环,谁先完成谁先执行,就好像下面的例子:

setTimeout(() => {}, 10)

setTimeout(() => {}, 30)

setTimeout(() => {}, 20)

mapSeries 方法是前一个定时器完成,即 10ms 的完成后调用 done 时,这个时候再开启 30ms 的定时器,当 30ms 的完成后再开 20ms 的定时器。这三个定时器不是在一个事件循环中被插入任务队列的,是三次独立事件循环。

最后再举一个不恰当的例子就很清晰了:

setTimeout(() => {
    
    setTimeout(() => {
        
        setTimeout(() => {}, 20)
    }, 30)
}, 10)

// 这个顺序就是 10 30 20 

四、总结

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

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

  2. collection 类型为对象或者数组时,这个时候根据 iterator.length 重载 iterator,即iterateor 是否需要对象的 key(或数组索引),三个形参时就传递给 iterator valkey(或数组index)、done,两个形参时传递给 iterator valdone ,函数的 length 属性表示的是形参个数;

  3. done 方法是最终结果 result 数组的保证的核心,再它的内部维护 completed 索引累加,当 done 被掉用时才会开启下一次的 iterator 迭代调用。另外,done 负责向 result 数组中添加数据(done(err, dataAddedToResult))是,done 内部通过 result[completed] = dataAddedToResult 的方式添加到result 数组,这种方式可以确保 result 中顺序与 iterator 执行的顺序有关且与各次 iterator异步完成顺序无关。

最后,要强调一点,我们现在说的 onWorkerMessage 是父进程收到子进程发来的消息时的处理逻辑,还没有开始说为子进程如何收到父进程的消息以及子进程何时传递消息给父进程。