為了適應異步編程,減少回調的嵌套,我嘗試了很多庫。最終覺得還是async最靠譜。
地址:https://github.com/caolan/async
Async的內容分為三部分:
- 流程控制:簡化十種常見流程的處理
- 集合處理:如何使用異步操作處理集合中的數據
- 工具類:幾個常用的工具類
本文介紹其中最簡單最常用的流程控制部分。
由於nodejs是異步編程模型,有一些在同步編程中很容易做到的事情,現在卻變得很麻煩。Async的流程控制就是為了簡化這些操作。
1. series(tasks, [callback]) (多個函數依次執行,之間沒有數據交換)
有多個異步函數需要依次調用,一個完成之后才能執行下一個。各函數之間沒有數據的交換,僅僅需要保證其執行順序。這時可使用series。
純js代碼:
step1(function(err, v1) {
step2(function(err, v2) {
step3(function(err, v3) {
// do somethig with the err or values v1/v2/v3
}
}
});
從中可以看到這嵌套還是比較多深的,如果再多幾步,會更深。在代碼中忽略對了每一層err的處理,否則還都等加上 if(err) return callback(err),那就更麻煩了。
對於這種情況,使用async來處理,就是這樣的:
var async = require(‘async’)
async.series([
step1, step2, step3
], function(err, values) {
// do somethig with the err or values v1/v2/v3
});
可以看到代碼簡潔了很多,而且自動處理每個回調中的錯誤。當然,這里只給出來最最簡單的例子,在實際中,我們常會在每個step中執行一些操作,這時可寫成:
var async = require(‘async’)
async.series([
function(cb) { step1(function(err,v1) {
// do something with v1
cb(err, v1);
}),
function(cb) { step2(...) },
function(cb) { step3(...) }
], function(err, values) {
// do somethig with the err or values v1/v2/v3
});
該函數的詳細解釋為:
- 依次執行一個函數數組中的每個函數,每一個函數執行完成之后才能執行下一個函數。
- 如果任何一個函數向它的回調函數中傳了一個error,則后面的函數都不會被執行,並且將會立刻會將該error以及已經執行了的函數的結果,傳給series中最后那個callback。
- 當所有的函數執行完后(沒有出錯),則會把每個函數傳給其回調函數的結果合並為一個數組,傳給series最后的那個callback。
- 還可以json的形式來提供tasks。每一個屬性都會被當作函數來執行,並且結果也會以json形式傳給series最后的那個callback。這種方式可讀性更高一些。
具體例子可參考:https://github.com/freewind/async_demo/blob/master/series.js
其代碼中還包含了:
- 如果中間某個函數出錯,series函數如何處理
- 如果某個函數傳給回調的值為undefined, null, {}, []等,series如何處理
另外還需要注意的是:多個series調用之間是不分先后的,因為series本身也是異步調用。
2. parallel(tasks, [callback]) (多個函數並行執行)
並行執行多個函數,每個函數都是立即執行,不需要等待其它函數先執行。傳給最終callback的數組中的數據按照tasks中聲明的順序,而不是執行完成的順序。
如果某個函數出錯,則立刻將err和已經執行完的函數的結果值傳給parallel最終的callback。其它未執行完的函數的值不會傳到最終數據,但要占個位置。
同時支持json形式的tasks,其最終callback的結果也為json形式。
示例代碼:
async.parallel([
function(cb) { t.fire('a400', cb, 400) },
function(cb) { t.fire('a200', cb, 200) },
function(cb) { t.fire('a300', cb, 300) }
], function (err, results) {
log(’1.1 err: ‘, err); // -> undefined
log(’1.1 results: ‘, results); // ->[ 'a400', 'a200', 'a300' ]
});
中途出錯的示例:
async.parallel([
function(cb) { log('1.2.1: ', 'start'); t.fire('a400', cb, 400) }, // 該函數的值不會傳給最終callback,但要占個位置
function(cb) { log('1.2.2: ', 'start'); t.err('e200', cb, 200) },
function(cb) { log('1.2.3: ', 'start'); t.fire('a100', cb, 100) }
], function(err, results) {
log(’1.2 err: ‘, err); // -> e200
log(’1.2 results: ‘, results); // -> [ , undefined, 'a100' ]
});
以json形式傳入tasks
async.parallel({
a: function(cb) { t.fire(‘a400′, cb, 400) },
b: function(cb) { t.fire(‘c300′, cb, 300) }
}, function(err, results) {
log(’1.3 err: ‘, err); // -> undefined
log(’1.3 results: ‘, results); // -> { b: ‘c300′, a: ‘a400′ }
});
更詳細示例參見:https://github.com/freewind/async_demo/blob/master/parallel.js
3. waterfall(tasks, [callback]) (多個函數依次執行,且前一個的輸出為后一個的輸入)
與seires相似,按順序依次執行多個函數。不同之處,每一個函數產生的值,都將傳給下一個函數。如果中途出錯,后面的函數將不會被執行。錯誤信息以及之前產生的結果,將傳給waterfall最終的callback。
這個函數名為waterfall(瀑布),可以想像瀑布從上到下,中途沖過一層層突起的石頭。注意,該函數不支持json格式的tasks。
async.waterfall([
function(cb) { log('1.1.1: ', 'start'); cb(null, 3); },
function(n, cb) { log('1.1.2: ',n); t.inc(n, cb); },
function(n, cb) { log('1.1.3: ',n); t.fire(n*n, cb); }
], function (err, result) {
log(’1.1 err: ‘, err); // -> null
log(’1.1 result: ‘, result); // -> 16
});
更詳細示例參見:https://github.com/freewind/async_demo/blob/master/waterfall.js
4. auto(tasks, [callback]) (多個函數有依賴關系,有的並行執行,有的依次執行)
用來處理有依賴關系的多個任務的執行。比如某些任務之間彼此獨立,可以並行執行;但某些任務依賴於其它某些任務,只能等那些任務完成后才能執行。
雖然我們可以使用async.parallel和async.series結合起來實現該功能,但如果任務之間關系復雜,則代碼會相當復雜,以后如果想添加一個新任務,也會很麻煩。這時使用async.auto,則會事半功倍。
如果有任務中途出錯,則會把該錯誤傳給最終callback,所有任務(包括已經執行完的)產生的數據將被忽略。
這里假設我要寫一個程序,它要完成以下幾件事:
- 從某處取得數據
- 在硬盤上建立一個新的目錄
- 將數據寫入到目錄下某文件
- 發送郵件,將文件以附件形式發送給其它人。
分析該任務,可以知道1與2可以並行執行,3需要等1和2完成,4要等3完成。
async.auto({
getData: function (callback) {
setTimeout(function(){
console.log(’1.1: got data’);
callback();
}, 300);
},
makeFolder: function (callback) {
setTimeout(function(){
console.log(’1.1: made folder’);
callback();
}, 200);
},
writeFile: ['getData', 'makeFolder', function(callback) {
setTimeout(function(){
console.log('1.1: wrote file');
callback(null, 'myfile');
}, 300);
}],
emailFiles: ['writeFile', function(callback, results) {
log('1.1: emailed file: ', results.writeFile); // -> myfile
callback(null, results.writeFile);
}]
}, function(err, results) {
log(’1.1: err: ‘, err); // -> null
log(’1.1: results: ‘, results); // -> { makeFolder: undefined,
// getData: undefined,
// writeFile: ‘myfile’,
// emailFiles: ‘myfile’ }
});
更多詳細示例參見:https://github.com/freewind/async_demo/blob/master/auto.js
5. whilst(test, fn, callback)(用可於異步調用的while)
相當於while,但其中的異步調用將在完成后才會進行下一次循環。舉例如下:
var count1 = 0;
async.whilst(
function() { return count1 < 3 },
function(cb) {
log(’1.1 count: ‘, count1);
count1++;
setTimeout(cb, 1000);
},
function(err) {
// 3s have passed
log(’1.1 err: ‘, err); // -> undefined
}
);
它相當於:
try {
whilst(test) {
fn();
}
callback();
} catch (err) {
callback(err);
}
該函數的功能比較簡單,條件變量通常定義在外面,可供每個函數訪問。在循環中,異步調用時產生的值實際上被丟棄了,因為最后那個callback只能傳入錯誤信息。
另外,第二個函數fn需要能接受一個函數cb,這個cb最終必須被執行,用於表示出錯或正常結束。
更詳細示例參見:https://github.com/freewind/async_demo/blob/master/whilst_until.js
6. until(test, fn, callback) (與while相似,但判斷條件相反)
var count4 = 0;
async.until(
function() { return count4>3 },
function(cb) {
log(’1.4 count: ‘, count4);
count4++;
setTimeout(cb, 200);
},
function(err) {
// 4s have passed
log(’1.4 err: ‘,err); // -> undefined
}
);
當第一個函數條件為false時,繼續執行第二個函數,否則跳出。
7. queue (可設定worker數量的隊列)
queue相當於一個加強版的parallel,主要是限制了worker數量,不再一次性全部執行。當worker數量不夠用時,新加入的任務將會排隊等候,直到有新的worker可用。
該函數有多個點可供回調,如worker用完時、無等候任務時、全部執行完時等。
定義一個queue,其worker數量為2,並在任務執行時,記錄一下日志:
var q = async.queue(function(task, callback) {
log(‘worker is processing task: ‘, task.name);
task.run(callback);
}, 2);
worker數量將用完時,會調用saturated函數:
q.saturated = function() {
log(‘all workers to be used’);
}
當最后一個任務交給worker執行時,會調用empty函數
q.empty = function() {
log(‘no more tasks wating’);
}
當所有任務都執行完時,會調用drain函數
q.drain = function() {
console.log(‘all tasks have been processed’);
}
放入多個任務,可一次放一個,或一次放多個
q.push({name:’t1′, run: function(cb){
log(‘t1 is running, waiting tasks: ‘, q.length());
t.fire(‘t1′, cb, 400); // 400ms后執行
}}, function(err) {
log(‘t1 executed’);
});
q.push([{name:'t3', run: function(cb){
log('t3 is running, waiting tasks: ', q.length());
t.fire('t3', cb, 300); // 300ms后執行
}},{name:'t4', run: function(cb){
log('t4 is running, waiting tasks: ', q.length());
t.fire('t4', cb, 500); // 500ms后執行
}}], function(err) {
log(‘t3/4 executed’);
});
更多詳細示例參見:https://github.com/freewind/async_demo/blob/master/queue.js
8. iterator(tasks) (將幾個函數包裝為iterator)
將一組函數包裝成為一個iterator,可通過next()得到以下一個函數為起點的新的iterator。該函數通常由async在內部使用,但如果需要時,也可在我們的代碼中使用它。
var iter = async.iterator([
function() { console.log('111') },
function() { console.log('222') },
function() { console.log('333') }
]);console.log(iter());
console.log(iter.next());
直接調用(),會執行當前函數,並返回一個由下個函數為起點的新的iterator。調用next(),不會執行當前函數,直接返回由下個函數為起點的新iterator。
對於同一個iterator,多次調用next(),不會影響自己。如果只剩下一個元素,調用next()會返回null。
更詳細示例參見:https://github.com/freewind/async_demo/blob/master/iterator.js
9. apply(function, arguments..) (給函數預綁定參數)
apply是一個非常好用的函數,可以讓我們給一個函數預綁定多個參數並生成一個可直接調用的新函數,簡化代碼。
對於函數:
function(callback) { t.inc(3, callback); }
可以用apply改寫為:
async.apply(t.inc, 3);
還可以給某些函數預設值,得到一個新函數:
var log = async.apply(console.log, ">");
log(‘hello’);
// > hello
更詳細代碼參見:https://github.com/freewind/async_demo/blob/master/apply.js
10. nextTick(callback) (在nodejs與瀏覽器兩邊行為一致)
nextTick的作用與nodejs的nextTick一樣,都是把某個函數調用放在隊列的尾部。但在瀏覽器端,只能使用setTimeout(callback,0),但這個方法有時候會讓其它高優先級的任務插到前面去。
所以提供了這個nextTick,讓同樣的代碼在服務器端和瀏覽器端表現一致。
var calls = [];
async.nextTick(function() {
calls.push(‘two’);
});
calls.push(‘one’);
async.nextTick(function() {
console.log(calls); // -> [ 'one', 'two' ]
});
更詳細代碼參見:https://github.com/freewind/async_demo/blob/master/nextTick.js