在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、匿名函数和闭包。