譯/異步代碼模式轉換(node)
作為傳統的同步多線程服務器的備選,異步事件IO被很多企業評估。異步意味著開發者需要學習新模式,忘掉老模式。轉換模式時需要忍受嚴重的大腦重新搭線,說不定電擊療法對此改變有幫助。
重布線
利用node工作,最基礎的是需要理解異步編程模式。我準備把異步代碼和同步代碼放在一起,對比的方式來學習新模式。案例都使用了fs模塊,因為它同時實現了同步和異步的兩種風格的庫函數。
回調
在node中,callback函數是異步事件驅動編碼的基本構造塊。它是作為參數傳遞給異步io操作的函數。它們在io操作完成后會被調用。比如fs模塊的readdir()就是一個異步io函數,它第一個參數為目錄名,第二個參數是一個callback 。當readdir()執行完畢,得到結果后,會調用這個callback,把結果經由callback的參數,傳遞給callback回調內。
依賴代碼和獨立代碼
下面的案例要讀取當前目錄的文件清單,打印文件名稱,讀出當前進程id。
同步版本:
var fs = require('fs'),
filenames,
i,
processId;
filenames = fs.readdirSync(".");
for (i = 0; i < filenames.length; i++) {
console.log(filenames[i]);
}
console.log("Ready.");
processId = process.getuid(); 異步版本:
var fs = require('fs'),
processId;
fs.readdir(".", function (err, filenames) {
var i;
for (i = 0; i < filenames.length; i++) {
console.log(filenames[i]);
}
console.log("Ready.");
});
processId = process.getuid(); 同步版本案例中,代碼等待 fs.readdirSync() I/O 完成。和我們日常的代碼類似。
打印文件名的代碼是依賴于fs.readdirSync()的結果的,而獲取進程id則獨立于此輸出。因此它們在新的異步版本代碼內需要放到不同位置。規則是把依賴代碼放到callback內,把獨立代碼放到原來的位置不動。
次序
同步代碼的標準模式是線性的,幾行代碼需要一個接一個的順序執行,因為每一個依賴于上一行的輸出。如下案例中,代碼首先修改文件訪問模式(類似unix chmod 命令)、然后重命名文件、然后檢查被命名文件是否為符號鏈接。顯然如果代碼不能按次序執行;或者文件在模式被修改前被重命名;或者檢查符號鏈接代碼在文件被命名前完成;這些都會導致錯誤。
同步:
var fs = require('fs'),
oldFilename,
newFilename,
isSymLink;
oldFilename = "./processId.txt";
newFilename = "./processIdOld.txt";
fs.chmodSync(oldFilename, 777);
fs.renameSync(oldFilename, newFilename);
isSymLink = fs.lstatSync(newFilename).isSymbolicLink(); 異步:
var fs = require('fs'),
oldFilename,
newFilename;
oldFilename = "./processId.txt";
newFilename = "./processIdOld.txt";
fs.chmod(oldFilename, 777, function (err) {
fs.rename(oldFilename, newFilename, function (err) {
fs.lstat(newFilename, function (err, stats) {
var isSymLink = stats.isSymbolicLink();
});
});
}); 異步代碼中,這個代碼的執行次序被翻譯為嵌套的callback。fs.lstat 回調被嵌套在fs.rename 回調內,fs.rename 回調被嵌入到fs.chmod()回調內
并行 Parallelisation
異步編碼特別適合io操作并發的場景:代碼執行不會被io調用所阻塞。多個io操作可以并行啟動。
如下案例:一個目錄的全部文件大小會被累加得到一個總計。使用同步代碼每次迭代都需要等待io操作返回單一文件的大小。異步代碼則可以啟動全部io操作,無需等待輸出。當io操作中的一個完成,callback就會被調用一次,文件大小會被累加。
同步
var fs = require('fs');
function calculateByteSize() {
var totalBytes = 0,
i,
filenames,
stats;
filenames = fs.readdirSync(".");
for (i = 0; i < filenames.length; i ++) {
stats = fs.statSync("./" + filenames[i]);
totalBytes += stats.size;
}
console.log(totalBytes);
}
calculateByteSize(); 異步
var fs = require('fs');
var count = 0,
totalBytes = 0;
function calculateByteSize() {
fs.readdir(".", function (err, filenames) {
var i;
count = filenames.length;
for (i = 0; i < filenames.length; i++) {
fs.stat("./" + filenames[i], function (err, stats) {
totalBytes += stats.size;
count--;
if (count === 0) {
console.log(totalBytes);
}
});
}
});
}
calculateByteSize(); 同步代碼是直截了當的,無需解釋。
異步版本代碼采用嵌套callback來保證調用次序,前節也已經提及。
有趣的地方在 fs.stat的回調函數內。它采用文件計數count作為完成條件。變量count初始化為文件總數,每次callback調用就遞減一次,一旦count等于0就說明全部io操作完成,合計文件大小被計算完畢。
異步代碼案例中還有一個有趣的特征:它使用了閉包。閉包是一個函數,它嵌入在另外一個函數內,并且內部函數能夠訪問了外部函數內聲明的變量,哪怕外部函數已經執行完成。fs.stat()的callback就是一個閉包,因為它訪問了在fs.readdir 的callback內聲明的count ,totalBytes 變量,哪怕這個callback早就已經執行完畢也可以訪問。閉包有自己的上下文,在這個上下文內可以把它要訪問的變量放置進來。沒有閉包的話,這兩個變量就必須設置為全局變量。因為fs.stat()的callback函數沒有任何可以放置變量的上下文。calculateBiteSize() 函數早早的就執行完畢也不能放置上下文,唯有全局的上下文還在。閉包就在這個場景下來救場的。在這樣的場合下,使用閉包就可以不必使用全局變量了。
代碼重用
可以抽取回調函數為單獨函數,可以達到代碼重用的效果。
下面的同步代碼案例,展示了一個countFiles函數,它可以返回給定目錄的文件數量。
同步:
var fs = require('fs');
var path1 = "./",
path2 = ".././";
function countFiles(path) {
var filenames = fs.readdirSync(path);
return filenames.length;
}
console.log(countFiles(path1) + " files in " + path1);
console.log(countFiles(path2) + " files in " + path2); 異步:
var fs = require('fs');
var path1 = "./",
path2 = ".././",
logCount;
function countFiles(path, callback) {
fs.readdir(path, function (err, filenames) {
callback(err, path, filenames.length);
});
}
logCount = function (err, path, count) {
console.log(count + " files in " + path);
};
countFiles(path1, logCount);
countFiles(path2, logCount); 替代fs.readdirSync()為異步版本的fs.readdir()帶來的一個效應,就是本來在同步版本代碼中的一個封閉的函數countFiles,現在也被迫變成一個帶有callback參數的異步函數。因為調用countFiles的代碼依賴這個函數的結果,而結果唯有等到fs.readdir()執行完畢。這就導致了countFiles的結構調整:不是console.log()調用countFiles,而是countFiles調用readdir(),后者完成后調用console.log 。
結論
本文強調了異步編程的基本模式。轉換到異步編程模式并不是微不足道的。恰恰相反,你需要一些時間去習慣它。復雜的提升帶來的回報是并行開發的復雜度戲劇化的被改善了。
Node的異步IO事件驅動模型,再加上靈動、易用性的JavaScript,Node.js 有機會把在企業應用市場打下一個烙印,特別是當在涉及到高度并行的Web2.0應用的子領域內。
參考:
Tim Caswell, Software developer at Creationix innovations | SlideShare - http://www.slideshare.net/creationix
</div> 原文 http://segmentfault.com/a/1190000002987030