在PHP代碼中,一個不能進行線性的很好的例子是衆多文档中的for語句。因爲沒有辦法消除for語句並且不能將阻塞的PHP API調用移動到它的外面去以使PHP代碼保持簡單的線性:
for ($i=0; $i < $n; ++$i){ $fp = fopen('fp.txt'+$i,'w'); fwrite($fp,$data); fclose($fp); }
Node.js的“回電函數鏈”說明了Node.js代碼如何工作。回調函數鏈是這樣構成的,每一個後續的非阻塞Node.js API調用都被嵌套在前一個非阻塞API調用的回調函數中,這樣構成了回調鏈,可以組合出任意長度的鏈:
fs.open('fp.txt0','w',0666,function(error,fp){ fs.write(fp,data,null,'utf-8',function(){ fs.close(fp,function(error){ fs.open('fp.txt1','w',0666,function(error,fp){ fs.write(fp,data,null,'utf-8',function(){ fs.close(fp,function(error){ // and on and on for a total of "n" times }); }); }); }); }); });
盡管這個例子展示了Node.js代碼如何完成這個功能,但是實際上n的值只有在運行時才能知道。爲了在Node.js代碼中實現回調函數鏈,必須使用到3個JavaScript的複雜特性:匿名函數、Lambda、閉包。
4.1 匿名函數、lambda、閉包
匿名函數就是沒有名稱的函數。命名函數是通用的工具,它們可以在任何時候被任何JavaScript代碼調用。相反,匿名函數通常是爲了特殊的目的並且只暴露給聲明它們的代碼。
如何調用一個沒有名稱的函數呢?匿名函數通常是一個lambda函數。lambda函數可以使用普通的函數調用語法通過它被賦值的變量驚醒調用。匿名函數被賦值給變量(或者被傳遞給函數作爲函數的參數)並且可以通過該變量使用函數調用語法調用。例如:
var f = function(a){ // lambda function return a + 1; }; var b = f(4);// b takes the value of 5
“lambda”這個詞聽起來有點誇張和難懂,但是它隱藏了一個簡單的概念,那就是函數是第一類值(first-class value),就像數字和字符型,並且它可以像對數字和字符串一樣處理和操作。
爲了使匿名函數更簡單並且功能更強大,JavaScript支持閉包。閉包是這樣一種語言特性,當函數被定義後,一個函數的外部上下文會被保存下來並且當函數被調用時提供給函數使用。在這個被保存的上下文中的任何變量的值都一直是持久化的並且同一時間只有一個值。對這個函數的所有調用共享同一個上下文並且引用同樣的變量。下面是閉包的例子:
function f(){ var b = 6; function g(){ //b id closed over by this function ++b; return b; } return g; } var h = f(); var c = h(); // c is 7 var d = h(); // d is 8
在這個例子中,變量b即使不是函數g()的本地變量,它仍然可以被函數g()使用,即使是在函數f()退出之後。變量b(在閉包的屬於中也被叫作“上值(upbalue)”的行爲就像是一個私有的全局變量。它對函數g()來說就像是一個全局變量但是對函數f()外面的所有代碼卻是隱藏的。
實現一個任意長度的回調函數鏈需要使用匿名函數、lambda和閉包:
var f = function(){ }; for (var i=n-1;i>=0;--i){ f = function(g,j){ return function(){ fs.open('fp.txt'+j,'w',0666,function(error,fp){ fs.write(fp,data,null,'utf-8',function(){ fs.close(fp,function(error){ g(); }); }); }); } }(f,i); } f();
這段Node.js代碼有一點複雜。整個算法是要構造一個逆序的回調函數鏈,從最深的函數嵌套調用到最外層的調用並且通過調用第一個函數來開始這個調用鏈。讓我們一步一步地把它過一遍。
創建一個不做任何事情的默認的lambda函數,並將其作爲最内層的fs.close()函數的“no-op”(空的)回調函數:
var f= function(){};
在PHP代碼中,for語句從0執行到n-1,但是因爲回調函數需要從内向外構建,因此在Node.js中需要for語句從n-1執行到0:
for(var i=n-1;i>=0;--i){ //create the callback }
當前的回調函數被保存在f變量中,它用來捕獲這個特定循環中的值。當前回調函數的外層回調函數會通過新的lambda函數創建,這個lambda函數會完成外層回調函數的工作並調用内層回調函數。接下來解釋爲什麽這個新的lambda函數可以作爲返回值:
return function(){ fs.open('fp.txt'+j,'w',0666,function(error,fp){ fs.write(fp.data,null,'utf-8',function(){ fs.close(fp,function(error){ g();//current callback }); }); }); }
這個新創建的回調函數作爲一個匿名函數被返回。匿名函數是一個未命名的函數,它可以被當作一個值使用。内層的匿名函數不需要參數並且它是當一個需要兩個參數的外層回調函數的返回值來使用的。外層函數被用來關閉閉包。
在這例子中,閉包必須被關閉,這樣f和i在這一次疊代中的值可以被保存下來,而不是在之後什麽時候的值被保存,比如在f函數被調用之後。通過制造新的變量可以關閉閉包,實際上在這裡是參數,它們的值總是一樣的。變量g保存了變量f在for循環的這次疊代中的值;同樣,變量j保存了這次疊代中變量i的值。外層的匿名函數(需要兩個參數的)通過跟在函數定義後面的(f,i)被標記爲立即執行。當它被調用後,變量f和變量i的值被捕獲到並且它將内存的匿名函數作爲返回值返回。内層的匿名函數實際上包含了這樣的回調函數代碼:
f = function(g,j){ return function(){ // implement outer callback // call g() to invoke inner callback }; }(f,i);
for循環從n-1執行到0,它創建出内部的回調函數並且把它們嵌套到外層的回調函數中。當foe循環退出是,變量f包含最外層的回調函數,它會調用隨後的回調函數,回調函數會以正確的順序被意義調用。現在剩下的僅僅是調用最外層的回調函數:
f();
這段代碼可以正常工作,但是它有兩個缺點。首先,PHP代碼和Node.js代碼開起來完全不一樣。如果回調函數經常發生(這是常見的),當兩份代碼區別非常大時,同時維護和改進PHP版本和Node.js版本會變得非常困難。其次,Node.js代碼本身相當複雜,它需要用到JavaScript的三個高級概念:lambda、匿名函數和閉包。