第三章 简单回调——3.1 线性

字号+ 编辑: Snake 修订: 面向ICU 来源: 《写给PHP开发者的Node.js学习指南》 2023-09-06 我要说两句(0)

在PHP和JavaScript所有的不同之中,最核心的一点:PHP在本质上来说是一种顺序执行的语言,而JavaScript则是事件驱动类型的。

在PHP和JavaScript所有的不同之中,最核心的一点:PHP在本质上来说是一种顺序执行的语言,而JavaScript则是事件驱动类型的。

不光是顺序执行或者事件驱动,都是在创建语言的时候所做的一种假设实现。任何一种语言都不是凭空产生的。PHP和JavaScript,都是为了某一个目的快速开发,然后在几个月内创建投入使用的。

1994年,Rasmus Lerdorf发明了PHP来替代CGI脚本语言。CGI脚本一般是C(或者C++)程序,用于实现在浏览器请求特定URL时,将程序的响应作为网页返回。这些C程序时单线程的,并且从头到尾按顺序执行。Lerdorf开发了PHP作为替代品。当浏览器请求一个特定URL时,不需要运行C程序,PHP脚本(页面)会很快编译并且执行,脚本的输出会作为响应的页面。PHP是单线程C程序的一个即兴的替代品,理所应当地采用了单线程的实现方式。

这些CGI脚本和早期的PHP页面并没有任何事件的概念,当时也没有这种需求。只有两个事件也许有意义并且有用处:(1)URL调用时间,(2)URL调用完成事件。URL调用事件通过运行PHP脚本处理,重点是,整个PHP脚本就是URL调用的处理。当PHP脚本执行完成,生成了所有的输出,可以被看作是URL调用完成事件。为了处理这种完成事件,需要在PHP页面末尾添加其他处理代码。所以PHP页面可以看作是两个事件之间的一大段无事件代码,像三明治一样。

所有这些关于事件的讨论只是为了解释PHP的实现方式和出路。没有任何多线程的概念或者:基本上,在做一件事情的时候需要在等待其他事情的完成,这种方式效率非常低。在PHP中,如果有一些任务需要耗费太长时间,你只能一直等待该任务完成,没有其他更快的

向下执行的方式。

CGI脚本和最早的PHP页面都是单线程的,是因为它们都假定了Web服务器会进行多线程的处理。对单独的 PHP 页面来说多线程并没有太大好处,但是如果一个Web服务器需要同时提供多个PHP页面,可以采用多线程来处理。PHP页面只是Web服务器中的独立的操作,配合Web应用工作,但是这些页面并不是Web服务器本身。

1995年Brendan Eich创建JavaScript时则与前一年出现的PHP有完全不同的目的和理念。设计JavaScript的目的是用于Netscape Navigator浏览器来处理时间。在浏览器中,用户需要点击按钮和链接,填写表格,在选择列表中进行选择,以及其他各种交互操作。相交于PH 毫无事件概念可言,JavaScript充满的各种事件。早期的网站中,开发人员需要捕获用户与页面交互过程中各种不同的事件,并为每个事件关联相应的处理来运行特定功能的代码。

在等待一个用户点击某个按钮的时候,浏览器不能停留在哪里一直等待。事实上,页面上有很多种控制(一直都有),这些控制的执行顺序和如何操作都是不可预测的。这一点与 PHP完全不同,PHP的执行顺序和数量都是完全可以预测的。

因为JavaScript是基于事件处理设计的,所以在一开始就有了在不同任务间切换的概念:从根本来说,就是在等待一件事情完成的同时做一些其他的事情。在JavaScript中,事件处理可以用于处理用户和浏览器的交互操作,也可以用于处理长时间操作的结束动作。很多操作都可以提供事件处理,开始,然后运行其它代码。当操作结束的时候,即使此时还有其他的JavaScript代码在运行,也会调用该操作完成的事件处理。

在2009年Ryan Dahl创建Node.js的时候,他觉得应该继承JavaScript的传统。他没有根据单线程页面的PHP建模,而是根据多任务多页面处理的Web服务器来建立模型。每个PHP页面代表一个独立工作的但愿,但是每个Node.js 脚本则可以代表整个Web服务器。事实上,创建一个Node.js脚本就可能提供任意数量的PHP页面服务。

但是我们这里的目标在于将PHP的功能转换为Node.js,并不是用Node.js创建一个Web服务器取代C程序,来提供 PHP 网页。

我们不需要在Node.js中创建PHP的处理机制,而是将整个PHP功能转换为Node.js,通过JavaScript运行,不需要在两种语言之间来回切换。PHP实现的所有功能都会由Node.js重新实现。

PHP和Node.js很多操作都很类似,包括一些相对复杂的操作,比如文件操作和数据库访问。有一点好处在于,PHP和JavaScript都是由C实现的,两者的开发人员都沿用了C语言的很多形式和惯例。而且更好的在于,在 PHP和 JavaScript产生的十五年后,Ryan Dahl借助JavaScript开发Node.js时,整个行业已经形成了一套大规模高度一致的标准操作,从简单的到复杂的操作,不仅仅是API看起来是什么样子如何使用,甚至包括特定的API命名应该是怎么样的。感谢所有Node.js开发人员的贡献,Node.js遵循了这些规范。

3.1 线性

我们用文件处理作为具体的例子,说明一下C、PHP和Node.js有很多相似。下面这段代码是一个文件叫作fp.txt,用于输出“Hello World!”:

#include<stdio.h>
FILE * fp = fopen(“fp.txt”,”w”);
fwrite(“Hello World!”,12,1,fp);
fclose(fp);

这段代码很简单。C语言的fopen() API 函数创建了一个文件,fwrite() API 将内容写入文件,而fclose()告诉函数库我们的文件的操作完成了。Fp变量是一个文件指针,用于保存当前操作的文件便于C API使用。引用studio.h文件程序才可以访问C文件系统的API。

以下是等价的PHP代码:

<?php
$fp = fopen('fp.txt',’w’);
fwrite($fp,’Hello World!’);
fclose($fp);
?>

可以看到,这与C的实现非常类似。PHP API函数使用相同的函数名,fopen()、fwrite()和fclose()。

再来看下Node.js的版本:

var fs = require('fs');
fs.open('fp.txt','w',0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    fs.close(fp,function(error){
    });
  });
});

Node.js和之前两种方式有类似也有不同。

Node.js需要使用Node.js fs模块来进行文件操作。与C类似但是和PHP不同,Node.js也需要声明来访问模块的文件处理函数。

Node.js API函数本身非常相似,但是命名稍有不同。在C和PHP中,打开文件都是用fopen()函数,但是在Node.js中,则调用了fs.oepn()。类似的,C和PHP中的fwrite()和fclose(),对英语Node.js中的fs.write()和fs.close()。

如果你用PHP的示例代码,并用Node.js API替换PHP API,会创建出一种PHP/Node.js混合代码。

$fp = fs.open('fp.txt',’w’);
fs.write($fp,’Hello World!’);
fs.close($fp);

其他一些小的不同很容易修改,比如去掉美元符号,添加Node.js需要的其他变量而PHP没有的。修改完成之后,PHP/Node.js混合代码看起来更像Node.js而不是PHP:

fp = fs.open('fp.txt',’w’,0666);
fs.write(fp,’Hello World!’,null,’utf-8’);
fs.close(fp);

还剩一个步骤。在Node.js中,fp变量并不是Node.js API函数fs.open()的返回值。它会作为参数传递给fs.open()最后一个参数回调函数。我们来修改一个调用,将fp变量放在正确的位置上:

fp = fs.open('fp.txt',’w’,0666,function(error,fp){
});
fs.write(fp,’Hello World!’,null,’utf-8’);
fs.close(fp);

Node.js fs.write() API函数无法再访问fp变量,因为它不在回调函数的作用域内。即使我们可以通过某种方式提供访问权限,Node.js fs.write()函数可能会过早调用,甚至在Node.js fs.open()调用之前,fp变量还没有从open函数中获取正确的值。

这段简单的代码突出了PHP到Node.js转换中最大的挑战。挑战在于,需要找到某种方式将不同源头的PHP 和Node.js整合到一起。单线程序列化的PHP需要适应多任务非序列化的JavaScript。PHP的无事件特性需要适应Node.js的事件,这样JavaScript代码才能工作。

PHP是由顺序执行的API组成的,也称作阻塞型。每条语句都是按顺序执行,下一条语句一定会等待上一条语句执行完才会开始执行。当调用PHP fopen()时,返回值包含文件指针,因为只有当文件真正打开的时候fopne()才回返回结果。如果硬盘太慢打开文件需要很长时间,API函数会等待,知道操作完成。

而Node.js更多的则是非阻塞型的API。每条语句依然是按顺序执行,但是当语句执行完毕的时候会将一个函数作为参数传给调用函数。顺序上来讲的下一条语句可能在这之前或者之后开始。

作为转换,至少需要讲阻塞型的PHP转换为非阻塞型的Node.js,下一条执行的语句并不是Node.js fs.open() API 调用之后的那一条,而是作为最后一个参数的回调函数的第一条语句。当我们进行转换时,我们需要将PHP代码中顺序执行的下一条代码放在回调函数里。

修改之后,代码如下所示:

fs.open('fp.txt',’w’,0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’);
});
fs.close(fp);

现在,fs.write()函数调用在回调函数内部。fs.write()函数直到fs.open()函数执行完毕后才会调用执行。fs.write()函数现在回调函作用域内,所以可以获取fp变量的值,而且我们按照API设计的方式正确使用,所以在fs.write()函数调用的时候可以确保fp变量的值是正确的。

很明显,这还没结束。fs.write()函数有自己的回调函数,fs.close()函数无法获取fp变量的值,就像fs.write()函数在放进回调函数之前的状况一样。解决方案与之前一样,对fs.write()函数添加回调函数,然后将fs.close()放在回调函数内:

fs.open('fp.txt',’w’,0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
  fs.close(fp);
});
});

最后,为fs.close()添加一个回调,调用自己来结束操作:

fs.open('fp.txt',’w’,0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    fs.close(fp,function(error){
    });
  });
});

原始的PHP代码中是三个阻塞型PHP API的调用。移植到Node.js之后,这三个阻塞型的调用转换为三个非阻塞型的Node.js调用。

这总让我想起嵌套:Node.js fs.open()函数调用扩展包含fs.write()函数调用,fs.write()函数又扩展包含fs.close()函数调用,fs.close()函数还可能进行扩展包含更多的代码。

最简单的情况下,一段顺序执行的PHP语句可以通过为等价的Node.js API函数添加回调函数的方式转换为Node.js,只需要拷贝其他的代码到回调中就可以了。

从这个角度来看,我们可以总结一个我称之为“转换清单”的内容。转换清单是一系列半机械化的步骤,可以用于将PHP代码转换为一种更能兼容Node.js的代码。

注意到我写的是“更兼容”。转换清单并不需要将可运行的PHP代码直接转换为可运行的Node.js代码。转换清单可能在一行代码中只定位一个问题,然后将半转换的代码作为PHP/Node.js的混合形式。

转换清单还有其他的一些重要的特性。

转换清单需要机械化或者半机械化执行,这样可以快速转换PHP代码而无需深入了解相关的代码。转换清单不应该遗漏任何信息或者代码,不能在之后需要重写代码。转换清单到保持代码的可读性,正确的顺序(即使不能工作),不应该使代码存在语法问题,比如缺少小括号或者花括号,或者把代码放在没有意义的地方。

文件处理的API并不是一个转换清单,只是用于解释转换中遇到的问题。如果用它作为一个转换清单,代码的顺序是混乱的。一下是一个基于我们所学过内容的转换清单。

原始代码如下:

$fp = fopen('fp.txt',’w’);
fwrite($fp,’Hello World!’);
fclose($fp);

将PHP fopen()转换为Node.js的fs.open()调用,修改调用函数的参数,将其余部分剪切到fs.open()函数的回调函数中,如下:

fs.open('fp.txt',’w’,0666,function(error,fp){
  fwrite(fp,’Hello World!’);
fclose(fp);
});

注意到fopen()已经转换为Node.js的fs.open(),但是回调函数中还是PHP代码。将PHP代码放在回调中保证了代码的执行顺序。

我们采用同样的步骤转换PHP的fwrite()函数。步骤是,转换fwrite()为Node.js的fs.write(),修改调用函数的参数,剪切剩下的代码粘贴到回调函数中:

fs.open('fp.txt',’w’,0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    fclose(fp);
  });
});

转换fclose()函数使用同样的方式,但是在fclose()操作之后没有其他操作了,所以创建一个空的回调函数:

fs.open('fp.txt',’w’,0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    fs.close(fp,function(error){
    });
  });
});

这是一个非常简单而且有效的转换清单。这本来可以作为本章的结束,但是:这个转换清单在稍微复杂一点的情况下很快就失败了。

这个转换清单可以工作是因为PHP是线性的。在没有类似if条件语句或者for

While循环的情况下代码是一行行按顺序执行的。我称之为“线性”,是因为可以想象出代码是从一行直线运行到下一行,没有交叉或者重组。

如果没有任何条件语句的存在,我们可以称之为PHP代码是完全线性的。一下就是一段完全线性的PHP代码:

$msg = 'start';
$fp = fopen('fp.txt',’w’);
$msg = 'opened';
fwrite($fp,’Hello World!’);
$msg = 'written';
fclose($fp);
$msg = 'closed';

转换清单对对线性PHP代码非常有效,所以很容易将上述PHP代码转换为以下代码;

var msg = 'start';
fs.open('fp.txt',’w’,0666,function(error,fp){
  msg = 'opened';
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    msg = 'written';
    fs.close(fp,function(error){
      mag = 'closed';
    });
  });
});

但是,如果PHP代码包含条件语句,PHP代码也可能是线性的,也就是说条件语句不会影响该转换清单的实施过程。为了具有线性特征,任何一个阻塞型的PHP调用转换成非阻塞型Node.js调用,从调用开始到代码结束都是线性的。

如果条件语句是独立的并且与阻塞型的PHP 调用分离,那么可以收PHP代码是具有线性特性的。例如:

$msg = 'start';
$fp = fopen('fp.txt',’w’);
if($fp !== null){
$msg = 'opened';
}
fwrite($fp,’Hello World!’);
fclose($fp);

if语句是独立的,并且在该语句之前、之中、之后的代码都是线性的。根据转换清单会生成以下的Node.js代码:

var msg = 'start';
fs.open('fp.txt',’w’,0666,function(error,fp){
  if(fp !== null){
  msg = 'opeed';
  }
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    fs.close(fp,function(error){
    });
  });
});

对于PHP代码来说,即使包含需要转换为Node.js的阻塞型PHP调用,如果所有的条件判断都保持线性,本质上那来说该代码也是线性的。例如:

$fp = fopen('fp.txt',’w’);
if($fp !== null){
  fwrite($fp,’Hello World!’);
  fclose($fp);
}

上述PHP代码很容易转换为以下Node.js代码:

fs.open('fp.txt',’w’,0666,function(error,fp){
  if(fp !== null){
    fs.write(fp,’Hello World!’,null,’utf-8’,function(){
      fs.close(fp,function(error){
    });
  });
}
});

并不是所有的代码都是线性的或者具有线性特征。有一些PHP代码就是非线性的。当代码交叉执行并且尝试用一种打破之前转换清单的方式重组时代码就是非线性的。当使用转换清单时,所有剩余代码都会被放置在Node.js API调用的回调函数之内。但是对非线性PHP代码,新的Node.js代码在回调函数外部也需要完全形同的代码,这样才能覆盖不使用非阻塞型的Node.js API的路径。在下述例子中可以更清楚地看到这点。

当一个阻塞型的PHP API调用在if语句模块中并且if语句模块结束后还有其他语句,PHP代码一定是非线性的。PHP的fwrite()调用可以使代码称为非线性的:

$fp = fopen('fp.txt',’w’);
if($msg == ‘say’){
  fwrite($fp,’Hello World!’);
}
$msg = 'done';

如果生搬硬套转换清单,Node.js的代码没办法编译甚至语法都是错误的!以下这段代码失效是因为只适用于线性PHP代码的转换清单应用在了非线性代码上:

fs.open('fp.txt',’w’,0666,function(error,fp){
  if(msg == 'say'){
    fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    }//opps!
    msg = 'done';//oops!
    });
  }
});

难点在于,最后一条语句,msg变量被设置为‘done’。这条语句既不在if语句模块之内也不属于fs.write(),将PHP代码从具有线性特征转换为了非线性。看起来这条语句很简单,并且与其他语句无关。但是最后一条语句将不同路径连结在了一起,它要求不管是执行if语句内部或者跳过if语句的代码都需要执行相同的一行代码,阻塞型的PHP API在一条路径中有回调函数而另一条路径中没有。如果删除最后一行语句,PHP代码就成为线性的了。并且,如果删除阻塞型的API调用或者换成其他没有回调函数的代码,PHP代码也会称为线性的。

直到现在,“代码的结束”仍然是个模糊的概念。

代码的结束是指PHP代码结束运行,PHP页面完成,Web服务器重新掌握控制权。每个PHP页面在运行时都有有限的生命周期,它从URL调用的伪事件运行到URL调用结束。最后的代码是在URL钓鱼能够结束的伪事件触发时执行的。

你可以看到,在你逆向工作的时候转换清单非常有效。如果你在PHP页面结束时开始,你可以逆向追踪,找到结束点,对每个结束点之前的先行部分按照转换清单操作。页面的入口和线性与转换清单无关。

比如if语句、for语句和while循环,即可以具有线性特征也可以是非线性的。一个有线性特征的语句可以是如下这种:

$total = 0;
$fp = fopen('fp.txt',’w’);
for($i=0; $i < 10; ++$i){
  $total +=$i;
}
fwrite($fp,’Total = ’.$total);

for语句与此无关,并且不影响转换清单的过程。一下Node.js可以正常工作:

var total = 0;
fs.open('fp.txt',’w’,0666,function(error,fp){
  for(var 1=0; I < 10; ++$i){
    total += i;
}
  fs.write(fp,’Total = ’+total,null,’utf-8’,function(){
  });
})

for语句也可以是非线性的。转换清单对以下PHP代码无效:

$fp = fopen('fp.txt',’w’);
for($i=0; $i < 3; ++$i){
 fwrite($fp,’Hello World!’);
}

如果你非要使用转换清单,Node.js代码会报语法错误,无法修复:

fs.open('fp.txt',’w’,0666,function(error,fp){
  for(var 1=0; I < 3; ++$i){
    fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    }//opps!
  });
});

经过一些练习,你可以训练你的眼睛很轻松地是被嗲吗的线性性,以此确定是否可以使用转换清单以及转换后的Node.js代码是什么样子。

在任何你想要转换的PHP页面中,你需要在页面的退出点只爱钱找到所有的线性代码,你想追溯代码执行路径知道你发现非线性的代码。

阅完此文,您的感想如何?
  • 有用

    0

  • 没用

    0

  • 开心

    0

  • 愤怒

    0

  • 可怜

    0

1.如文章侵犯了您的版权,请发邮件通知本站,该文章将在24小时内删除;
2.本站标注原创的文章,转发时烦请注明来源;
3.交流群: PHP+JS聊天群

相关课文
  • JS如何防止父节点的事件运行

  • nodejs编写一个简单的http请求客户端代码demo

  • 使用Sublime Text3 开发React-Native的配置

  • 说一则为什么后端开发人员不选择node.js的原因

我要说说
网上宾友点评