本系列文章使用的async版本為v1.5.0.
JS的非同步函式執行,大致上可以分為以下幾種:
- 所有非同步任務並行執行
- 無最大並行數限制
- 有最大並行數限制
- 所有非同步任務序列執行
- 序列執行與並行執行相結合
1. 並行執行(無最大並行數限制)
這是最簡單的一種非同步執行流程。只需要一個簡單的arr.forEach
就可以完成。
考慮如下的一種情形:給定一系列的檔名,分別讀取檔案內容並輸出,當讀取完所有的檔案後,提示任務完成。
為了得知是否所有任務都已經執行完,我們引入一個初始值為0的計數器,每當一個任務完成的時候,就給計數器加1,然後檢測計數器的值是否等於輸入的檔案個數,如果相等,則說明所有任務執行完畢。
簡單的程式碼實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
var files = ['1.txt', '2.txt', '3.txt', '4.txt', '5.txt']; var i = 0; files.forEach(function(file) { fs.readFile(file, function(err, data) { if (err) { return callback(err); } console.log(data.toString()); callback(); }); }); function callback(err) { i++; if (err) { throw err; } if (i === files.length) { console.log('all done!'); } } |
但是每次遇到這種情況,都需要手動維護計數器比較麻煩,因此需要將這個過程封裝為一個函式,從而方便呼叫。我們期望的函式呼叫是如下形式:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
// eachOf(arr, fn, callback) eachOf(files, function(val, key, callback) { fs.readFile(val, function(err, data) { if (err) { return callback(err); } console.log(data.toString()); callback(); }) }, function(err) { if (err) { throw err; } console.log('add done!'); }); |
注意:
eachOf
的第三個引數callback
和fn
的第三個引數callback
是不同的函式。
fn
的callback
是由使用者手動呼叫的,而eachOf
的callback
是在所有任務完成後由eachOf
函式內部自動呼叫的。
即對於arr
中的每一項,呼叫fn
,當所有任務執行完後,呼叫callback
。eachOf
的初步實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function eachOf(arr, fn, callback) { callback = callback || function() {}; arr = arr || []; if (!arr.length) { callback(); } var i = 0; arr.forEach(function(val, i) { fn(val, i, done); }); function done(err) { i++; if (err) { return callback(err); } if (i === arr.length) { callback(); } } } |
在這裡也看以看出,fn
的callback
其實是內部提供的done
函式。但是這樣還存在著一個潛在的問題,如果在fn
中多次呼叫了callback
,那麼當一個任務完成後,就會多次呼叫done
,此時計數器就無法正確計數。因此,需要確保每個任務完成後,無論使用者呼叫多少次callback
,done
都只能執行一次。
為此,實現一個once
函式,如下:
1 2 3 4 5 6 7 8 9 |
function once(fn) { return function() { if (!fn) { return; } fn.apply(this, arguments); fn = null; }; } |
然後將fn(val, i, done);
替換為fn(val, i, once(done));
即可。
在async中,提供瞭如下方法:
1 2 3 4 5 6 7 8 9 10 11 |
async.forEachOf = async.eachOf = function(object, iterator, callback) { // ... ... }; // iterator(val, key, callback) async.forEach = async.each = function(arr, iterator, callback) { // ... ... }; // iterator(val, callback) |
它們的第一個引數不僅可以是一個陣列,還可以是類陣列或者物件。它們的區別只是在於iterator
的函式簽名不同。
2. 並行執行(有最大並行數限制)
相當於有一個池子(類比於執行緒池),當池子不飽和的時候,就向裡面加入任務。當所有任務都加入到了池子後,等待所有任務完成,然後執行最後的回撥函式。如果在執行過程中出錯,那麼不再繼續執行後面的任務。
因此,需要三個變數:
i
表示當前任務的序號running
表示當前正在執行的任務的個數errored
表示是否出錯
簡單實現如下:
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 41 42 43 44 45 46 47 48 49 50 |
function eachOfLimit(limit, arr, fn, callback) { callback = once(callback || function() {}); arr = arr || []; if (limit <= 0) { return callback(); } var i = -1; var running = 0; var errored = false; replenish(); function replenish() { // 當所有任務都已經加入到池子,且當前沒有正在執行的任務 // 說明所有任務都執行完畢,執行callback if (i === arr.length && running <= 0) { return callback(); } // 只要當前正在執行的任務個數小於限制數,且沒有出錯 // 就繼續向池子中新增任務 while (running < limit && !errored) { // 如果沒有任務可以新增,且沒有任務正在執行 // 說明所有任務都執行完畢,執行callback // 這裡不需要對running>0的情況進行處理 // 因為在done中會replenish,最終會進入上面的if判斷 if (++i === arr.length) { if (running <= 0) { callback(); } return; } running++; fn(arr[i], i, once(done)); } } function done(err) { running--; if (err) { callback(err); errored = true; } else { replenish(); } } } |
例子如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
eachOfLimit(2, files, function(val, key, callback) { fs.readFile(val, function(err, data) { if (err) { return callback(err); } console.log(data.toString()); callback(); }) }, function(err) { if (err) { throw err; } console.log('all done!'); }); |
在async中,提供瞭如下方法:
1 2 3 4 5 6 7 8 9 10 11 |
async.forEachOfLimit = async.eachOfLimit = function(obj, limit, iterator, callback) { // ... ... }; // iterator(val, key, callback) async.forEachLimit = async.eachLimit = function(arr, limit, iterator, callback) { // ... ... }; // iterator(val, callback) |
3. 序列執行
序列執行也就是說只有當一個任務完成後,才會繼續執行下一個任務。比較典型的就是Express中介軟體的執行。
這種情況下通常依賴於一個next
函式,該函式用來取出一個任務並執行,當任務完成後,遞迴呼叫next
從而繼續下一個任務的執行。
簡單實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
function eachOfSeries(arr, fn, callback) { callback = once(callback || function() {}); arr = arr || []; var i = -1; next(); function next() { if (++i === arr.length) { return callback(); } fn(arr[i], i, once(done)); } function done(err) { if (err) { return callback(err); } next(); } } |
呼叫如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
eachOfSeries(files, function(val, key, callback) { fs.readFile(val, function(err, data) { if (err) { return callback(err); } console.log(data.toString()); callback(); }); }, function(err) { if (err) { throw err; } console.log('all done!'); }); |
但是這樣的實現還有一個小問題,當fn
內部都是同步操作時,例如:
1 2 3 4 5 6 7 8 9 10 |
eachOfSeries(files, function(val, key, callback) { console.log(val, ': pre callback'); callback(); console.log(val, ': post callback'); }, function(err) { if (err) { throw err; } console.log('all done!'); }); |
此時輸出為:
1 2 3 4 5 6 7 8 9 10 11 |
1.txt : pre callback 2.txt : pre callback 3.txt : pre callback 4.txt : pre callback 5.txt : pre callback all done! 5.txt : post callback 4.txt : post callback 3.txt : post callback 2.txt : post callback 1.txt : post callback |
為了解決這個問題,引入一個sync
變數,用來表示當前處於同步執行還是非同步執行。修改後的程式碼如下:
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 |
function eachOfSeries(arr, fn, callback) { callback = once(callback || function() {}); arr = arr || []; var i = -1; var sync = true; next(); function next() { sync = true; if (++i === arr.length) { return callback(); } fn(arr[i], i, once(done)); sync = false; } function done(err) { if (err) { return callback(err); } if (sync) { setImmediate(next); } else { next(); } } } |
在done
中對sync
進行判斷,如果sync
為true
,則說明fn
是一個同步操作,此時需要setImmediate(next)
,將下一次呼叫變為非同步操作。
在async中,提供瞭如下方法:
1 2 3 4 5 6 7 8 9 10 11 |
async.forEachOfSeries = async.eachOfSeries = function(obj, iterator, callback) { // ... ... }; // iterator(val, key, callback) async.forEachSeries = async.eachSeries = function(arr, iterator, callback) { // ... ... }; // iterator(val, callback) |