持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的13天,点击查看活动详情
一、前情回顾
上一篇小作文文讨论 onWorkerMessage 的方法核心逻辑,并就 job 类型进行了展开讨论,其他类型做了简单说明:
本篇小作文我们要详细讨论在 onWokerMessage 处理 message.type 为 job 使用到的 neo-async/mapSeries 方法的核心实现,以及如何确保异步串行的结果的顺序保证。
二、 neo-async/mapSeries 方法
在上面的 onWorkerMessage 方法中,我们看到在 message.type 为 job 时调用 asyncMapSeries 方法进程异步的遍历。现在我们来说说这个方法,类似前面说 neo-async/queue 一样,说清楚这个方法会更容易立即 thread-loader 做的工作。
-
方法位置:
node_modules/neo-async/mapSeries.js -
方法参数:
- 2.1
collection:待遍历的集合对象,是一个数组、对象或者其他部署了遍历器接口的对象; - 2.2
iterator:迭代器方法,为collection中的每一项调用一次这个iterator方法; - 2.3
callback:回调函数,当遍历完成后要执行的回调方法,接收迭代处理的结果;
- 2.1
-
方法作用:异步串行遍历
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 中干了一件事,开启一个定时器延时分别为 10ms,30ms,20ms;按照大家的概念,执行顺序是 10,20,30 的,但是你会发现,他的答案却不是这样的,而是 10,30,20。
他的诀窍在定时器中调用的 done 方法,从上面的源码中可以看出来,done 是上一个异步完成后才会调用,而 done 的调用会给保证 result 顺序的 completed 索引累加并且执行下一次迭代。
什么意思呢?也就是说10,30,20 这三个定时器不是一次性开启的,如果一次性开启三个定时器,那肯定按照事件循环,谁先完成谁先执行,就好像下面的例子:
setTimeout(() => {}, 10)
setTimeout(() => {}, 30)
setTimeout(() => {}, 20)
mapSeries 方法是前一个定时器完成,即 10ms 的完成后调用 done 时,这个时候再开启 30ms 的定时器,当 30ms 的完成后再开 20ms 的定时器。这三个定时器不是在一个事件循环中被插入任务队列的,是三次独立事件循环。
最后再举一个不恰当的例子就很清晰了:
setTimeout(() => {
setTimeout(() => {
setTimeout(() => {}, 20)
}, 30)
}, 10)
// 这个顺序就是 10 30 20
四、总结
本篇小作文详细讨论 neo-async/mapSeries 方法的原理:
-
接收
collection对象,可以是对象、数组或者其他部署了迭代器接口的对象; -
当
collection类型为对象或者数组时,这个时候根据iterator.length重载iterator,即iterateor 是否需要对象的key(或数组索引),三个形参时就传递给 iteratorval、key(或数组index)、done,两个形参时传递给 iteratorval、done,函数的length属性表示的是形参个数; -
done方法是最终结果result数组的保证的核心,再它的内部维护completed索引累加,当done被掉用时才会开启下一次的iterator迭代调用。另外,done负责向result数组中添加数据(done(err, dataAddedToResult))是,done内部通过result[completed] = dataAddedToResult的方式添加到result数组,这种方式可以确保result中顺序与iterator执行的顺序有关且与各次iterator中异步完成顺序无关。
最后,要强调一点,我们现在说的 onWorkerMessage 是父进程收到子进程发来的消息时的处理逻辑,还没有开始说为子进程如何收到父进程的消息以及子进程何时传递消息给父进程。