前言
最近在看Node設計模式之非同步程式設計的順序非同步迭代,簡單的實現如下:
function series(tasks, callback) {
let results = [];
function iterate(index) {
if (index === tasks.length) {
return finish();
}
const task = tasks[index];
task(function(err, res) {
results.push(res);
iterate(index + 1);
});
}
function finish() {
// 迭代完成的操作
callback(null, results);
}
iterate(0);
}
series(
[
callback => {
setTimeout(function() {
console.log(456);
callback(null, 1);
}, 500);
},
callback => {
console.log(123);
callback(null, 2);
}
],
function(err, results) {
console.log(results);
}
);
// 456
// 123
// [1, 2]
複製程式碼
而async庫是一個非常流行的解決方案,在Node.js和JavaScript中來說,用於處理非同步程式碼。它提供了一組功能,可以大大簡化不同配置中一組任務的執行,併為非同步處理集合提供了有用的幫助。
async庫可以在實現複雜的非同步控制流程時大大幫助我們,但是一個難題就是選擇正確的庫來解決問題。例如,對於順序執行,有大約20個不同的函式可供選擇。
好奇心起來,就想看看一個成熟的庫跟我們簡單實現的程式碼區別有多大。
series
按順序執行任務集合中的函式,每個函式在前一個函式完成後執行。如果系列中的任何函式將錯誤傳遞給其回撥函式,則不會執行更多函式,並立即使用錯誤值呼叫回撥函式。否則,回撥會在任務完成時收到一系列結果。
const async = require(`async`);
async.series({
one: function(callback) {
setTimeout(function() {
callback(null, 1);
}, 200);
},
two: function(callback){
setTimeout(function() {
callback(null, 2);
}, 100);
}
}, function(err, results) {
console.log(results);
// results is now equal to: {one: 1, two: 2}
});
複製程式碼
我們來看看原始碼,找到series方法,可以看到:
function series(tasks, callback) {
_parallel(eachOfSeries, tasks, callback);
}
複製程式碼
除了我們自己傳的兩個引數以外,預設還傳了一個eachOfSeries,接著往下看:
function _parallel(eachfn, tasks, callback) {
// noop:空的函式
callback = callback || noop;
// isArrayLike:檢查`value`是否與array相似
var results = isArrayLike(tasks) ? [] : {};
eachfn(tasks, function (task, key, callback) {
// wrapAsync:包裝成非同步
wrapAsync(task)(function (err, result) {
if (arguments.length > 2) {
result = slice(arguments, 1);
}
results[key] = result;
callback(err);
});
}, function (err) {
callback(err, results);
});
}
複製程式碼
這裡我們可以看到,_parallel方法其實就是eachOfSeries方法的呼叫。
先解釋一下eachOfSeries這三個引數:
- 第一個引數就是要執行的函式的集合。
- 第二個引數可以看成每個函式的執行(wrapAsync可以先忽略掉,直接看成這一個函式)。
- 第三個引數就是所有函式執行完後的回撥。
讓我們來看看eachOfSeries是如何的實現:
var eachOfSeries = doLimit(eachOfLimit, 1);
function eachOfLimit(coll, limit, iteratee, callback) {
_eachOfLimit(limit)(coll, wrapAsync(iteratee), callback);
}
function doLimit(fn, limit) {
return function (iterable, iteratee, callback) {
return fn(iterable, limit, iteratee, callback);
};
}
複製程式碼
我們把上面進行轉換,這樣看起來更明瞭些:
var eachOfSeries = function(iterable, iteratee, callback) {
return _eachOfLimit(1)(iterable, wrapAsync(iteratee), callback);
};
複製程式碼
Soga,最終就是呼叫_eachOfLimit完成的:
// limit:一次非同步操作的最大數量,傳1可以看成序列,一個函式執行完才進行下一個
function _eachOfLimit(limit) {
return function (obj, iteratee, callback) {
// once:函式只執行一次
callback = once(callback || noop);
if (limit <= 0 || !obj) {
return callback(null);
}
// iterator:迭代器,有根據型別分類,這邊簡單拿陣列迭代器createArrayIterator來分析
var nextElem = iterator(obj);
var done = false;
var running = 0;
var looping = false;
function iterateeCallback(err, value) {
running -= 1;
if (err) {
done = true;
callback(err);
}
else if (value === breakLoop || (done && running <= 0)) {
done = true;
return callback(null);
}
else if (!looping) {
replenish();
}
}
function replenish () {
looping = true;
while (running < limit && !done) {
var elem = nextElem();
if (elem === null) {
done = true;
if (running <= 0) {
callback(null);
}
return;
}
running += 1;
// onlyOnce:函式只執行一次
iteratee(elem.value, elem.key, onlyOnce(iterateeCallback));
}
looping = false;
}
// 遞迴
replenish();
};
}
function once(fn) {
return function() {
if (fn === null) return;
var callFn = fn;
fn = null;
callFn.apply(this, arguments);
};
}
// 閉包大法,拿取集合中的函式
function createArrayIterator(coll) {
var i = -1;
var len = coll.length;
return function next() {
return ++i < len ? {value: coll[i], key: i} : null;
}
}
複製程式碼
終於,看到series的真身了。實現其實就是replenish()的遞迴大法。因為要實現序列,所以在replenish()中控制running數為1,取出集合中一個函式執行,然後回撥iterateeCallback(),running數減1,再呼叫replenish(),這樣就能控制每個函式在前一個函式完成後執行。
說起來這流程還是比較簡單,但是在非同步程式設計裡還是不太好理解,我們先來了解一下js執行機制,再舉一個例子來看:
js執行機制
- 同步的進入主執行緒,非同步的進入Event Table並註冊函式。
- 當指定的事情完成時,Event Table會將這個函式移入Event Queue。
- 主執行緒內的任務執行完畢為空,會去Event Queue讀取對應的函式,進入主執行緒執行。
- 上述過程會不斷重複,也就是常說的Event Loop(事件迴圈)。
普通版
function a() {
setTimeout(function() {
console.log(456);
}, 500);
}
function b() {
console.log(123);
}
function c() {
setTimeout(function() {
console.log(789);
}, 0);
}
a();
b();
c();
// 123
// 789
// 456
複製程式碼
按順序執行可以看到
- a()中setTimeout進入Event Table,註冊回撥函式,計時開始。
- b(),執行console.log(123)。
- c()中setTimeout進入Event Table,註冊回撥函式,計時開始。
- c()中setTimeout先完成,回撥函式進入Event Queue。
- 500ms到了,c()中setTimeout完成,回撥函式進入Event Queue。
- 主執行緒從Event Queue讀取回撥函式並執行。
series版
const async = require(`async`);
async.series(
[
callback => {
setTimeout(function() {
console.log(456);
callback(null, 1);
}, 500);
},
callback => {
console.log(123);
callback(null, 2);
},
callback => {
setTimeout(function() {
console.log(789);
callback(null, 3);
}, 0);
}
],
function(err, results) {
console.log(results);
}
);
// 456
// 123
// 789
// [ 2, 1, 3 ]
複製程式碼
按我自己的理解,主執行緒和Event Loop都執行完稱為一輪:
-
第一輪
- 按照上面流程,主執行緒走到_eachOfLimit(),呼叫replenish()。根據while迴圈(執行數running < 一次非同步操作的最大數量 limit),running += 1,進入集合中第一個函setTimeout數的呼叫,setTimeout進入Event Table,註冊回撥函式。
- 回到while迴圈,running=limit,結束迴圈,結束主執行緒。
- setTimeout事件完成,回撥函式進入Event Queue。
- 主執行緒從Event Queue讀取回撥函式並執行,回撥iterateeCallback,running -= 1,呼叫replenish()。
-
第二輪
- 重複第一輪。只要的區別在於集合中的第二個函式是同步的,所有是主執行緒一路執行下來。
-
第三輪
- 重複第一輪。
-
第四輪
- 集合中的三個函式已經都執行完了,通過iterator()閉包拿到是null,回撥最終結果。
wrapAsync
function wrapAsync(asyncFn) {
return isAsync(asyncFn) ? asyncify(asyncFn) : asyncFn;
}
var supportsSymbol = typeof Symbol === `function`;
function isAsync(fn) {
return supportsSymbol && fn[Symbol.toStringTag] === `AsyncFunction`;
}
複製程式碼
wrapAsync()先判斷是否非同步函式,如果是es7 Async Functions的話呼叫asyncify,否則返回原函式。
function asyncify(func) {
return initialParams(function (args, callback) {
var result;
try {
result = func.apply(this, args);
} catch (e) {
return callback(e);
}
// if result is Promise object
if (isObject(result) && typeof result.then === `function`) {
result.then(function(value) {
invokeCallback(callback, null, value);
}, function(err) {
invokeCallback(callback, err.message ? err : new Error(err));
});
} else {
callback(null, result);
}
});
}
var initialParams = function (fn) {
return function (/*...args, callback*/) {
var args = slice(arguments);
var callback = args.pop();
fn.call(this, args, callback);
};
};
複製程式碼
採用同步功能並將其設定為非同步,並將其返回值傳遞給回撥函式。如果傳遞給asyncify的函式返回一個Promise,則該Promise的resolved/rejected狀態將用於呼叫回撥,而不僅僅是同步返回值。
總結
平日用慣async-await、promise,用起來簡單,但也導致缺少思考。而嘗試用原生js去模擬,閱讀原始碼,卻能帶來更多的收穫。
github地址,喜歡的支援star一下,Thanks♪(・ω・)ノ。