8. series/parallel/parallelLimit
async.eachOfSeries(arr, iterator, callback)
是對arr
中的每一項,呼叫iterator
函式,最終呼叫callback
。也就是說,所有的非同步任務都是同一種型別,只是傳入的引數不同。例如對於一個目錄下的所有檔案,統計每個檔案的size。
因此async.eachOfSeries
無法處理多個不同型別的非同步任務的情況,例如多個非同步任務,一個是請求遠端資料,一個是判斷檔案是否存在,還有一些其它型別的任務。這種情況下需要一個新的函式series
。
series
是基於async.eachOfSeries
來實現,基本邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function series(tasks, callback) { callback = callback || function() {}; var results = Array.isArray(tasks) ? [] : {}; async.eachOfSeries(tasks, function(task, key, callback) { task(function(err, data) { // [2] results[key] = data; // [3] callback(err); // [4] }); }, function(err) { // [5] callback(err, results); // [6] }); } // example series([function(callback) { fs.access('index.js', function(err) { callback(null, err ? false : true); // [1] }); }, function(callback) { fs.readFile('package.json', function(err, data) { callback(err, data); // [1] }); }], function(err, results) { if (err) { throw err; } console.log(results); }); |
1 |
<span style="font-weight: normal">這裡有很多callback,比較迷惑人。現在將它們分類如下:</span> |
- 每個task的回撥,記作
taskCallback
async.eachOfSeries
第二個引數中的回撥,記作iteratorCallback
async.eachOfSeries
的第三個引數也是一個回撥(相關程式碼參看eachOfSeries
的實現),記作eachOfSeriesCallback
series
的第二個引數,記作seriesCallback
現在來分析series
的執行流程:
- 依次執行每個task,在這個過程中,會由使用者呼叫
taskCallback
([1]) taskCallback
其實是在series
的實現中定義([2])- 在執行
taskCallback
的時候,會先將task的結果儲存在結果集合中([3]),然後呼叫iteratorCallback
([4]) iteratorCallback
的作用是判斷任務是否執行完畢,如果沒有執行完畢,則繼續執行下一個任務;如果任務執行完畢,則執行eachOfSeriesCallback
(該部分的邏輯在async.eachOfSeries
中實現)- 當所有task執行完畢後,或者任務執行過程中出錯時,會執行
eachOfSeriesCallback
([5]) - 在
eachOfSeriesCallback
中,執行seriesCallback
([6]),至此,series
的執行過程結束
parallel/parallelLimit
與series
的實現類似,只不過是將async.eachOfSeries
換成了async.eachOf/async.eachOfLimit
來實現。
9. waterfall
series模型的多個任務之間是沒有依賴關係的。但是如果一個任務的執行依賴於上一個任務的結果,series
就無法處理了,此時需要使用waterfall
,即每個任務的執行結果會傳遞給下一個任務。
一個簡單的實現思路是:將每一步任務的結果儲存在一個變數中,然後將該變數傳入下一個任務作為引數。基本邏輯如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
function waterfall(tasks, callback) { callback = callback || function() {}; tasks = tasks || []; // 由於傳入下一個任務的引數可能不止一個,因此使用一個陣列來儲存中間值 var result = []; var slice = result.slice; async.eachOfSeries(tasks, function(task, key, callback) { result.push(function(err) { result = slice.call(arguments, 1); callback(err); }); task.apply(this, result); }, function(err) { result.unshift(err); callback.apply(this, result); }); } // example waterfall([function(callback) { fs.readFile('package.json', function(err, data) { callback(err, data); }); }, function(data, callback) { try { var pkgInfo = JSON.parse(data.toString()); fs.readFile(pkgInfo.main, function(err, data) { callback(err, data); }); } catch (err) { callback(err); } }], function(err, result) { if (err) { throw err; } console.log(result.toString()); }); |
當然,這只是一個非常簡陋的實現,僅僅用來描述waterfall的實現邏輯。在async的原始碼中,async.waterfall的實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
async.waterfall = function(tasks, callback) { callback = _once(callback || noop); // 由於任務之間存在嚴格的依賴關係,因此tasks必須是一個陣列,而不能是物件 // 這裡判斷比較嚴格,即使是類陣列物件也不可以 if (!_isArray(tasks)) { var err = new Error('First argument to waterfall must be an array of functions'); return callback(err); } // 如果任務列表為空,就直接執行回撥 if (!tasks.length) { return callback(); } // 該部分程式碼會在下面詳細分析 function wrapIterator(iterator) { return _restParam(function(err, args) { if (err) { callback.apply(null, [err].concat(args)); } else { var next = iterator.next(); if (next) { args.push(wrapIterator(next)); } else { args.push(callback); } ensureAsync(iterator).apply(null, args); } }); } wrapIterator(async.iterator(tasks))(); }; |
首先來看async.iterator
,它的作用是將一個任務列表轉換成一個迭代器結構,例如:
1 2 3 4 5 6 7 8 9 10 |
var iterator = async.iterator([ function(){console.log('1');}, function(){console.log('2');}, function(){console.log('3');}, function(){console.log('4');} ]); iterator()()()(); // 1, 2, 3, 4 iterator.next().next()(); // 3 iterator().next().next()(); // 1, 4 |
1 2 3 4 5 6 7 8 9 10 |
var iterator = async.iterator([ function(){console.log('1');}, function(){console.log('2');}, function(){console.log('3');}, function(){console.log('4');} ]); iterator()()()(); // 1, 2, 3, 4 iterator.next().next()(); // 3 iterator().next().next()(); // 1, 4 |
該函式原始碼不難看懂,在此不做贅述。
接下來看wrapIterator
,其中有一個_restParam
比較影響閱讀,我們把該函式改寫為如下形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// example async.waterfall([function f1(callback) {callback(null, 'data1');}, function f2(data1, callback) {callback(null, 'data21', 'data22');}, function f3(data21, data22, callback) {callback(null, 'data3');} ], function cb(err, data3) {}); ///////////////////// // 這裡iterator表示task,即例子中的f1,f2,f3(事實上是async.iterator封裝後的task) function wrapIterator(iterator) { return function(err, args) { // args表示傳給當前task的引數,也就是上一個任務傳過來的結果 args = [].slice.call(arguments, 1); if (err) { // 如果執行過程中出錯,則直接到最終的callback callback.apply(null, [err].concat(args)); } else { var next = iterator.next(); if (next) { // 如果接下來還有任務,則將下一個任務通過wrapIterator包裝後push到args中 // 即wrapIterator包裝後的下一個任務,作為當前任務的最後一個引數 // 也就是作為當前任務的callback // 因此下一個任務收集到的args就是當前任務傳過去的結果 // 例如上面例子中當執行callback(null, 'data1')的時候,其實是在遞迴執行該函式 // 因此args就能收集到'data1'傳遞給f2了 args.push(wrapIterator(next)); } else { // 如果接下來沒有任務了,就將最後的callback作為當前任務的回撥 args.push(callback); } // 執行當前任務 // 使用ensureAsync來確保任務非同步執行 ensureAsync(iterator).apply(null, args); } } } |
總的來說,wrapIterator
返回了一個可以遞迴呼叫的函式,從而可以依次執行任務。這段程式碼比較不容易理解,但是實現的非常巧妙。
接下來看ensureAsync
的實現(已經改寫為去掉_restParam
的形式從而方便閱讀):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
function ensureAsync(fn) { return function() { var args = [].slice.call(arguments); var callback = args.pop(); args.push(function() { var innerArgs = arguments; if (sync) { // 如果使用者同步呼叫了callback,則會進入該分支 // 例如: // async.waterfall([function(callback){ // callback(); // }], function(){}) async.setImmediate(function() { callback.apply(null, innerArgs); }); } else { // 如果使用者非同步呼叫了callback,則會進入該分支 // 例如: // async.waterfall([function(callback){ // fs.readFile('index.txt',callback); // }],function(){}) callback.apply(null, innerArgs); } }); var sync = true; fn.apply(this, args); sync = false; } } |
這與async.eachOfSeries
中的實現是基本一致的。那麼為什麼要確保任務的執行是非同步呢?這是因為:在一個任務中呼叫callback
會去執行下一個任務,假如所有的任務都是同步呼叫,而且任務非常多,那麼就會導致棧溢位。
關於該情況下棧溢位問題,可以通過如下例子來模擬:
12345678910111213 function simulateOverflow(n) {var fns = [];fns.push(function() {});for (var i = 1; i < n; i++) {var fn = (function(k) {return function() {fns[k - 1]();};})(i);fns.push(fn);}fns[n - 1]();}如果
n
非常大,例如simulateOverflow(100000)
,就會導致如下錯誤:
1 RangeError: Maximum call stack size exceeded如果將
fns[k - 1]()
改成setImmediate(fns[k - 1])
,就不會有問題了。