走近Node.js的異步代碼設(shè)計(jì)
譯文【51CTO精選譯文】許多企業(yè)目前在評(píng)估Node.js的異步、事件驅(qū)動(dòng)型的I/O,認(rèn)為這是一種高性能方案,可以替代多線程企業(yè)應(yīng)用服務(wù)器的傳統(tǒng)同步I/O。異步性質(zhì)意味著,企業(yè)開發(fā)人員必須學(xué)習(xí)新的編程模式,忘掉舊的編程模式。他們必須徹底轉(zhuǎn)變思路,可能需要借助電擊療法^_^。本文介紹了如何將舊的同步編程模式換成全新的異步編程模式。
51CTO推薦專題:Node.js專區(qū)
開始轉(zhuǎn)變思路
要使用Node.js,就有必要了解異步編程的工作原理。異步代碼設(shè)計(jì)并非簡(jiǎn)單的設(shè)計(jì),需要一番學(xué)習(xí)?,F(xiàn)在需要來(lái)一番電擊療法:本文在同步代碼示例旁邊給出了異步代碼示例,表明如何更改同步代碼,才能變成異步代碼。這些示例都圍繞Node.js的文件系統(tǒng)(fs)模塊,因?yàn)樗俏ㄒ缓型絀/O操作及異步I/O操作的模塊。有了這兩種示例,你可以開始轉(zhuǎn)變思路了。
相關(guān)代碼和獨(dú)立代碼
回調(diào)函數(shù)(callback function)是Node.js中異步事件驅(qū)動(dòng)型編程的基本構(gòu)建模塊。它們是作為變量,傳遞給異步I/O操作的函數(shù)。一旦操作完成,回調(diào)函數(shù)就被調(diào)用?;卣{(diào)函數(shù)是Node.js中實(shí)現(xiàn)事件的機(jī)制。
下面顯示的示例表明了如何將同步I/O操作轉(zhuǎn)換成異步I/O操作,并顯示了回調(diào)函數(shù)的使用。示例使用異步fs.readdirSync()調(diào)用,讀取當(dāng)前目錄的文件名稱,然后把文件名稱記錄到控制臺(tái),***讀取當(dāng)前進(jìn)程的進(jìn)程編號(hào)(process 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.");
- processprocessId = 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.");
- });
- processprocessId = process.getuid();
在同步示例中,處理器等待fs.readdirSync() I/O操作,所以這是需要更改的操作。Node.js中該函數(shù)的異步版本是fs.readdir()。它與fs.readdirSync()一樣,但是回調(diào)函數(shù)作為第二個(gè)參數(shù)。
使用回調(diào)函數(shù)模式的規(guī)則如下:把同步函數(shù)換成對(duì)應(yīng)的異步函數(shù),然后把原先在同步調(diào)用后執(zhí)行的代碼放在回調(diào)函數(shù)里面。回調(diào)函數(shù)中的代碼與同步示例中的代碼執(zhí)行一模一樣的操作。它把文件名稱記錄到控制臺(tái)。它在異步I/O操作返回之后執(zhí)行。
就像文件名稱的記錄依賴fs.readdirSync() I/O操作的結(jié)果,所列文件數(shù)量的記錄也依賴其結(jié)果。進(jìn)程編號(hào)的存儲(chǔ)獨(dú)立于I/O操作的結(jié)果。因而,必須把它們移到異步代碼中的不同位置。
規(guī)則就是將相關(guān)代碼移到回調(diào)函數(shù)中,而獨(dú)立代碼的位置不用管。一旦I/O操作完成,相關(guān)代碼就被執(zhí)行,而獨(dú)立代碼在I/O操作被調(diào)用之后立即執(zhí)行。
順序
同步代碼中的標(biāo)準(zhǔn)模式是線性順序:幾行代碼都必須下一行接上一行來(lái)執(zhí)行,因?yàn)槊恳恍写a依賴上一行代碼的結(jié)果。在下面示例中,代碼首先變更了文件的訪問(wèn)模式(比如Unix chmod命令),對(duì)文件更名,然后檢查更名后文件是不是符號(hào)鏈接。很顯然,該代碼無(wú)法亂序運(yùn)行,不然文件在模式變更前就被更名了,或者符號(hào)鏈接檢查在文件被更名前就執(zhí)行了。這兩種情況都會(huì)導(dǎo)致出錯(cuò)。因而,順序必須予以保留。
同步
- 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();
- });
- });
- });
在異步代碼中,這些順序變成了嵌套回調(diào)。該示例顯示了fs.lstat()回調(diào)嵌套在fs.rename()回調(diào)里面,而fs.rename()回調(diào)嵌套在fs.chmod()回調(diào)里面。
#p#
并行處理
異步代碼特別適合操作I/O操作的并行處理:代碼的執(zhí)行并不因I/O調(diào)用的返回而受阻。多個(gè)I/O操作可以并行開始。在下面示例中,某個(gè)目錄中所有文件的大小都在循環(huán)中累加,以獲得那些文件占用的總字節(jié)數(shù)。使用異步代碼,循環(huán)的每次迭代都必須等到獲取單個(gè)文件大小的I/O調(diào)用返回為止。
異步代碼允許快速連續(xù)地在循環(huán)中開始所有I/O調(diào)用,不用等結(jié)果返回。只要其中一個(gè)I/O操作完成,回調(diào)函數(shù)就被調(diào)用,而該文件的大小就可以添加到總字節(jié)數(shù)中。
唯一必不可少的有一個(gè)恰當(dāng)?shù)耐V箻?biāo)準(zhǔn),它決定著我們完成處理后,就計(jì)算所有文件的總字節(jié)數(shù)。
同步
- 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();
同步示例簡(jiǎn)單又直觀。在異步版本中,***個(gè)fs.readdir()被調(diào)用,以讀取目錄中的文件名稱。在回調(diào)函數(shù)中,針對(duì)每個(gè)文件調(diào)用fs.stat(),返回該文件的統(tǒng)計(jì)信息。這部分不出所料。
值得關(guān)注的方面出現(xiàn)在計(jì)算總字節(jié)數(shù)的fs.stat()回調(diào)函數(shù)中。所用的停止標(biāo)準(zhǔn)是目錄的文件數(shù)量。變量count以文件數(shù)量來(lái)初始化,倒計(jì)數(shù)回調(diào)函數(shù)執(zhí)行的次數(shù)。一旦數(shù)量為0,所有I/O操作都被回調(diào),所有文件的總字節(jié)數(shù)被計(jì)算出來(lái)。計(jì)算完畢后,字節(jié)數(shù)可以記錄到控制臺(tái)。
異步示例有另一個(gè)值得關(guān)注的特性:它使用閉包(closure)。閉包是函數(shù)里面的函數(shù),內(nèi)層函數(shù)訪問(wèn)外層函數(shù)中聲明的變量,即便在外層函數(shù)已完成之后。fs.stat()回調(diào)函數(shù)是閉包,因?yàn)樗缭趂s.readdir()回調(diào)函數(shù)完成后,訪問(wèn)在該函數(shù)中聲明的count和totalBytes這兩個(gè)變量。閉包有關(guān)于它自己的上下文。在該上下文中,可以放置在函數(shù)中訪問(wèn)的變量。
要是沒(méi)有閉包,count和totalBytes這兩個(gè)變量都必須是全局變量。這是由于fs.stat()回調(diào)函數(shù)沒(méi)有放置變量的任何上下文。calculateBiteSize()函數(shù)早已結(jié)束,只有全局上下文仍在那里。這時(shí)候閉包就能派得上用場(chǎng)。變量可以放在該上下文中,那樣可以從函數(shù)里面訪問(wèn)它們。
代碼復(fù)用
代碼片段可以在JavaScript中復(fù)用,只要把代碼片段包在函數(shù)里面。然后,可以從程序中的不同位置調(diào)用這些函數(shù)。如果函數(shù)中使用了I/O操作,那么改成異步代碼時(shí),就需要某種重構(gòu)。
下面的異步示例顯示了返回某個(gè)目錄中文件數(shù)量的函數(shù)countFiles()。countFiles()使用I/O操作fs.readdirSync() 來(lái)確定文件數(shù)量。span style="font-family: courier new,courier;">countFiles()本身被調(diào)用,使用兩個(gè)不同的輸入?yún)?shù):
同步
- 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()迫使閉包函數(shù)cntFiles()也變成異步,因?yàn)檎{(diào)用cntFiles()的代碼依賴該函數(shù)的結(jié)果。畢竟,只有fs.readdir()返回后,結(jié)果才會(huì)出現(xiàn)。這導(dǎo)致了cntFiles()重構(gòu),以便還能接受回調(diào)函數(shù)。整個(gè)控制流程突然倒過(guò)來(lái)了:不是console.log()調(diào)用cntFiles(),cntFiles()再調(diào)用fs.readdirSync(),在異步示例中,而是cntFiles()調(diào)用fs.readdir(),然后cntFiles()再調(diào)用console.log()。
結(jié)束語(yǔ)
本文著重介紹了異步編程的一些基本模式。將思路轉(zhuǎn)變到異步編程絕非易事,需要一段時(shí)間來(lái)適應(yīng)。雖然難度增加了,但是獲得的回報(bào)是顯著提高了并發(fā)性。結(jié)合JavaScript的快速周轉(zhuǎn)和易于使用等優(yōu)點(diǎn),Node.js中的異步編程有望在企業(yè)應(yīng)用市場(chǎng)取得進(jìn)展,尤其是在新一代高度并發(fā)性的Web 2.0應(yīng)用程序方面。
原文:http://shinetech.com/thoughts/thought-articles/139-asynchronous-code-design-with-nodejs
【編輯推薦】