JavaScript異步編程:異步數(shù)據(jù)收集的具體方法
Asyncjs/seriesByHand.js
var fs = require('fs');
process.chdir('recipes'); // 改變工作目錄
var concatenation = '';
fs.readdir('.', function(err, filenames) {
if (err) throw err;
function readFileAt(i) {
var filename = filenames[i];
fs.stat(filename, function(err, stats) {
if (err) throw err;
if (! stats.isFile()) return readFileAt(i + 1);
fs.readFile(filename, 'utf8', function(err, text) {
if (err) throw err;
concatenation += text;
if (i + 1 === filenames.length) {
// 所有文件均已讀取,可顯示輸出
return console.log(concatenation);
}
readFileAt(i + 1);
});
});
}
readFileAt(0);
});
如你所見,異步版本的代碼要比同步版本多很多。如果使用filter、forEach這些同步方法,代碼的行數(shù)大約只有一半,而且讀起來也要容易得多。如果這些漂亮的迭代器存在異步版本該多好啊!使用Async.js就能做到這一點!
何時拋出亦無妨?
大家可能注意到了,在上面那個代碼示例中筆者無視了自己在第1.4節(jié)中提出的建議:從回調(diào)里拋出異常是一種糟糕的設(shè)計,尤其在成品環(huán)境中。不過,一個簡單如斯的示例直接拋出異常則完全沒有問題。如果真的遇到代碼出錯的意外情形,throw會關(guān)停代碼并提供一個漂亮的堆棧軌跡來解釋出錯原因。
這里真正的不妥之處在于,同樣的錯誤處理邏輯(即if(err) throw err)重復(fù)了多達(dá)3次!在4.2.2節(jié),我們會看到Async.js如何幫助減少這種重復(fù)。
Async.js的函數(shù)式寫法
我們想把同步迭代器所使用的filter和forEach方法替換成相應(yīng)的異步方法。Async.js給了我們兩個選擇。
async.filter和async.forEach,它們會并行處理給定的數(shù)組。
async.filterSeries和async.forEachSeries,它們會順序處理給定的數(shù)組。
并行運行這些異步操作應(yīng)該會更快,那為什么還要使用序列式方法呢?原因有兩個。
前面提到的工作流次序不可預(yù)知的問題。我們確實可以先把結(jié)果存儲成數(shù)組,然后再joining(聯(lián)接)數(shù)組來解決這個問題,但這畢竟多了一個步驟。
Node及其他任何應(yīng)用進(jìn)程能夠同時讀取的文件數(shù)量有一個上限。如果超過這個上限,操作系統(tǒng)就會報錯。如果能順序讀取文件,則無需擔(dān)心這一限制。
所以現(xiàn)在先搞明白async.forEachSeries再說。下面使用了Async.js的數(shù)據(jù)收集方法,直接改寫了同步版本的代碼實現(xiàn)。
Asyncjs/forEachSeries.js
var async = require('async');
var fs = require('fs');
process.chdir('recipes'); // 改變工作目錄
var concatenation = '';
var dirContents = fs.readdirSync('.');
async.filter(dirContents, isFilename, function(filenames) {
async.forEachSeries(filenames, readAndConcat, onComplete);
});
function isFilename(filename, callback) {
fs.stat(filename, function(err, stats) {
if (err) throw err;
callback(stats.isFile());
});
}
function readAndConcat(filename, callback) {
fs.readFile(filename, 'utf8', function(err, fileContents) {
if (err) return callback(err);
concatenation += fileContents;
callback();
});
}
function onComplete(err) {
if (err) throw err;
console.log(concatenation);
}
現(xiàn)在我們的代碼漂亮地分成了兩個部分:任務(wù)概貌(表現(xiàn)形式為async.filter調(diào)用和async.forEachSeries調(diào)用)和實現(xiàn)細(xì)節(jié)(表現(xiàn)形式為兩個迭代器函數(shù)和一個完工回調(diào)onComplete)。
filter和forEach并不是僅有的與標(biāo)準(zhǔn)函數(shù)式迭代方法相對應(yīng)的Async.js工具函數(shù)。Async.js還提供了以下方法:
reject/rejectSeries,與filter剛好相反;
map/mapSeries,1:1變換;
reduce/reduceRight,值的逐步變換;
detect/detectSeries,找到篩選器匹配的值;
sortBy,產(chǎn)生一個有序副本;
some,測試是否至少有一個值符合給定標(biāo)準(zhǔn);
every,測試是否所有值均符合給定標(biāo)準(zhǔn)。
這些方法是Async.js的精髓,令你能夠以最低的代碼重復(fù)度來執(zhí)行常見的迭代工作。在繼續(xù)探索更高級的方法之前,我們先來看看這些方法的錯誤處理技術(shù)。
Async.js的錯誤處理技術(shù)
要怪就怪Node的fs.exists首開這一先河吧!而這也意味著使用了Async.js數(shù)據(jù)收集方法(filter/filterSeries、reject/rejectSeries、detect/detectSeries、some、every等)的迭代器均無法報告錯誤。
對于非布爾型的所有Async.js迭代器,傳遞非null/undefined的值作為迭代器回調(diào)的首參數(shù)將會立即因該錯誤值而調(diào)用完工回調(diào)。這正是readAndConcat不用throw也能工作的原因。
Asyncjs/forEachSeries.js
function readAndConcat(filename, callback) {
fs.readFile(filename, 'utf8', function(err, fileContents) {
if (err) return callback(err);
concatenation += fileContents;
callback();
});
}
所以,如果callback(err)確實是在readAndConcat中被調(diào)用的,則這個err會傳遞給完工回調(diào)(即onComplete)。Async.js只負(fù)責(zé)保證onComplete只被調(diào)用一次,而不管是因首次出錯而調(diào)用,還是因成功完成所有操作而調(diào)用。
Asyncjs/forEachSeries.js
function onComplete(err) {
if (err) throw err;
console.log(concatenation);
}
Node的錯誤處理約定對Async.js數(shù)據(jù)收集方法而言也許并不理想,但對于Async.js的所有其他方法而言,遵守這些約定可以讓錯誤干凈利落地從各個任務(wù)流向完工回調(diào)。下一節(jié)會看到更多這樣的例子。