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