第三章 简单回调——3.2 线性化代码

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

有时候,非线性代码可以冲狗狗为线性代码。不管你想同步PHP代码和Node.js,或者抛弃PHP只使用Node.js,都值得我们花费一些时间来重构代码,这样代码中只包含最少的非线性代码。

看一下之前的非线性代码的例子:

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

if语句模块中包含PHP API调用,而语句执行完之后又汇总到同一条路经,所以这段代码是非线性的。如果可以去掉这两个非线性条件之一,这段代码就可以具有线性特征了。当移除非线性的时候,需要思考语句的不同路径的删除方式。

对if语句来说有两条路径:true和false。因为路径较少,所以一般采取的方式是简单地拷贝其余部分的代码。如果可以并且这部分代码不太长,重复的代码可以隔离不同的路径,使代码线性化。之前的非线性PHP代码会成为以下线性代码:

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

添加了else语句,重复了msg变量的赋值语句。代码现在是线性的了,应用转换清单进行转换,如下:

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

使用重复的手法重构if语句是一个经常使用的创建线性的好办法。添加一个else语句(如果没有的话)然后拷贝粘贴其余的语句分别到if和else语句中就可以了。

其他一些情况下,可以深入理解API本身的调用,重构减少PHP的API调用来移除非线性。看一下之前非线性的for语句示例:

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

在这个例子中,值得指出的是,找出问题和寻求可能的解决方案是非令人苦恼的过程。当循环展开的时候,很明显fs.write()调用不能继续放在循环里,需要链式嵌套在回调函数中:

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

如果循环次数是一个离散的数字,循环可以被展开。但是如果循环次数是一个变量,就不可能再以这种方式展开循环了。

幸运的是,通过研究fwrite() PHP API,我们了解到多次调用fwrite() API可以通过一次调用写入所有数据替换。所以PHP代码重构后,如下所示:

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

需要写入文件的数据都放在data变量中,用一次fwrite() API调用写入文件。重构的代码具有线性特征,线性的PHP代码可以使用转换清单:

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

一行行分析PHP代码,找出非线性的代码并重构为非线性的过程,是将PHP代码转换到Node.js准备过程的第一步,是必要步骤,值得我们花点时间。到现在,你已经学习了如何将简单语句、条件语句和阻塞型PHP调用转换为更容易移植的PHP代码,最终称为非阻塞型的Node.js代码。但是PHP代码中处理语句和API调用之外还有其他的内容:比如函数和函数调用。

如果你还记得,本章开始的时候我们曾说过,Node.js是如何在整个行业多年形成的共识上建立自己内建函数库和API的模型,可以一直追溯到原始C API 的命名、函数和操作。当尝试如何将阻塞型的PHP API调用转换为非阻塞型的Node.js API时,基本依赖于每一个PHP API找到类似的Node.js API。这种方式绝大部分时候都行得通,即使行不通,也很容易在现有的Node.js API的基础上构建需要的Node.js API,一样可以工作。

但是,现有代码中的PHP函数,相对于内建的阻塞型PHP函数来说,要难处理得多。很难预测这些函数是否使用阻塞性的PHP API,所以很难决定是否需要回调函数。

来看一个用户自定义的PHP函数,prefixAndConcat()。该函数接收两个参数:字符串和数组,返回一个字符串。返回值是一个长字符串,将字符串参数拼接在每个数组元素之前,然后将所有拼接字符串连在一起。例如,字符串参数为prefix_,数组参数为['a','b','c'],返回的结果为prefix_aprefix_bprefix_c:

function prefixAndConcat($str,$a){
  //unknow code that may or may not call blocking PHP APIS
}
$n = prefixAndConcat('prefix_',array('a','b','c'));
$msg = 'prefixd string: '.$n; //last statement before PHP exit point

将prefixAndConcat()函数与另一个用户自定义的PHP函数进行对比,调用该函数可以输出多次“Hello World!”到某一个文件,并将返回值保存在msg变量中:

function writeMultipleStrings($str,$a){
  //unknow code that may or may not call blocking PHP APIS
}
$n = writeMultipleStrings('fp.txt',array('a','b','c'));
$msg = 'wrote '.$n.'times'; //last statement before PHP exit point

这两个函数看起来非常相似。但是,prefixAndConcat()函数只操作字符串,即使在Node.js中也不会需要非阻塞型的API调用。但是,writeMultipleStrings()函数则需要使用非阻塞型的Node.js文件处理API。

当转换为Node.js时,prefixAndConcat()函数的调用如下:

var n = prefixAndConcat('prefix_',array('a','b','c'));
var msg = 'prefixd string: '+n; //last statement before PHP exit point

作为对比,writeMultipleStrings()函数的调用如下:

writeMultipleStrings('fp.txt',array('a','b','c'),function(){
  var msg = 'wrote'+n+'times';//last statement before PHP exit point
});

对我们而言的挑战是,需要将PHP表达式的返回值作为Node.js回调函数的参数来追踪什么时候PHP和Node.js代码维持一致并且同步。这是另一个需要跟踪的非线性的Node.js函数,并理解它如何影响代码的线性特征。

一种处理提供符合规范的命名,可以通过命名传递用户定义的函数在Node.js中是阻塞或者非阻塞的(PHP中总是阻塞型的)。在函数名中添加些信息,可以更容易地在浏览转换钱的PHP代码时预计到转换后的Node.js看起来是什么样子。一个转换方法是在每个调用非阻塞型Node.js API的PHP自定义函数名末尾加上_cb(cb表示callback)。

所以,writeMultipleStrings()重命名为writeMultipleStrings_cb():

function writeMultipleStrings_cb($str,$a){
  //unknow code that may or may not call blocking PHP APIS
}

如果按照这种方式转换,很明显,writeMultipleStrings_cb()的Node.js调用代码为:

writeMultipleStrings_cb('fp.txt',array('a','b','c'),function(n){
  var msg = 'wrote'+n+'times';//last statement before PHP exit point
});

prefixAndConcat()函数不发生任何变化,名字里也没有_cb:

var n = prefixAndConcat('prefix_',['a','b','c']);
var msg = 'prefixd string: '+n; //last statement before PHP exit point

这个转换方法只对相对简单的PHP代码有效,但是在较复杂的PHP代码中,几乎每个用户自定义的函数都会使用阻塞型的PHP API。对每个这样的函数添加_cb并没有太大用处。只有一些PHP函数名包含_cb而另一些没有,才会使这个标识符有意义。

另一种更有效的转换策略是限制包括阻塞型PHP API调用函数的嵌套。如果一个PHP用户自定义的函数A调用了另一个函数B,函数B又调用了函数C,以此类推,我们称这种为深层嵌套。如果调用只有一层或者两层,则称为浅嵌套。在一般的PHP代码中,如果目的不是移植到Node.js,是需要深层次嵌套的,因为低层次的函数需要高层次函数产生的细节。但是,分析和移除PHP的非线性,维持与Node.js版本的同步,则需要浅嵌套关系。当处理非线性问题时需要在各个层面理解代码,这是必需的。抽象在这种时候是个障碍,不是帮助。

删除抽象,使PHP代码更容易分析和重构为线性代码的一种方式是展开嵌套的PHP用户自定义函数。这种转换方式移除嵌套关系,更容易对PHP代码进行分析和重构。思考一下writeMultipleStrings()函数的实现。之前的部分介绍过函数的用途,但是以下是该函数的实际实现:

function writeMultipleStrings_cb($str,$a){
  $fp = fopen($str,'w');
  $data = '';
  for($i=0;$i<count($a);++$i){
    $data .=$a[$a];
  }
  fwrite($fp,$data);
  fclose($fp);
  return $data;
}
$n = writeMultipleStrings('fp.txt',array('a','b','c'));
$msg = 'wrote'.$n.'data';//last statement before PHP exit point

为了展开该函数,拷贝粘贴writeMultipleStrings()的实现到调用的地方。如果可以保留变量名,则可以在实现的开始声明变量并且赋值。在实现的末尾将返回值赋给该变量:

$str = 'fp.txt';//argument #1
$a = array('a','b','c');//argument #2
$fp = fopen($str,'w');
$data = '';
for($i=0;$i<count($a);++$i){
  $data .=$a[$i];
}
fwrite($fp,$data);
fclose($fp);
$n = $data;//return value
$m  = 'wrote'.$n.'data';//last statement before PHP exit point

展开writeMultipleStrings()调用之后,很容易使这段代码线性化。对应的Node.js代码为:

var str = 'fp.txt';//argument #1
var a = ['a','b','c'];//argument #2
fs.open($str,'w',0666,function(error,fp){
  var data = '';
  for (var i=0;i<a.length;++i){
    data += a[i];
  }
  fs.write(fp,data,null,'utf-8',function(){
    fs.close(fp,function(){
     var n = data;//return value
     var msg  = 'wrote'.$n.'data';//last statement before PHP exit point
    });
  });
});

你可以看到,展开PHP函数调用可以减少嵌套的层次,是代码更容易移植。

还有第二种减少嵌套层次方法不太好实现,通过检视PHP代码,看看有没有可能减少嵌套关系,集中注意力在那个可能调用阻塞型PHP API的用户自定义函数上。那些没有调用阻塞型PHP API的自定义函数则没有关系。通过仔细的检查和重构来避免不必要的自定义函数,PHP带代码移植到Node.js的过程或更容易些。

如本章介绍的一样,PHP到Node.js的代码移植过程的一个重要的方面就是重构PHP代码,使其更容易进行移植。在PHP代码中可以很好地处理线性,所以Node.js和PHP的关系是平行并且紧密的。

我们的预期是PHP和Node.js代码可以保持同步并且并行改进,开发人员可以根据自己对线性特性和其他一些Node.js转换问题的理解来编写新的特性,同时关注保持两个代码库之间的转换。

代码的线性化是我们的目标,但并不是一定都能实现的。本章中介绍了一些基本的转换方法,将非线性的PHP代码转换为线性。但是还是有些非线性的代码需要更复杂的转换策略来处理。

一些for语句非常有挑战性。当一个需要移植到Node.js的for语句包含阻塞型PHP API调用,需要转换为一个非阻塞型的Node.js API调用,就会产生线性的问题:

$fp = fopen($str,'w');
for($i=0;$i<count($a);++$i){
  fwrite($fp,'Hello World!');
}

当这段代码转换为Node.js时,for语句完全消失,而由一个回调函数链的结构代替:

fs.open(‘fp.txt’,’w’,0666,function(error,fp){
  fs.write(fp,’Hello World!’,null,’utf-8’,function(){
    fs.write(fp,’Hello World!’,null,’utf-8’,function(){
      fs.write(fp,’Hello World!’,null,’utf-8’,function(){
        //and on and on for a total of "n" times
      });
    });
  });
});

本章开始的部分介绍了一种转换方法来删除循环中的阻塞型PHP API调用。深入研究一下Node.js fs.write() API时如何工作的,阻塞性的PHP API 调用可以移到for语句之外。所以不需要将for语句转换为一个回调函数链,可以保留for语句,但是只在一条路径上进行迭代,循环结束后,调用一次Node.js fs.write()。关键之处在于重构代码,将多次Node.js fs.write()调用改为一次。没有了Node.js回调函数链,代码功能相同,看起来更像原始的PHP代码:

fs.open($str,'w',0666,function(error,fp){
  var data = '';
  for (var i=0;i<n;++i){
    data += 'Hello World!';
  }
  fs.write(fp,data,null,'utf-8',function(){
  });
});

如果深入研究没有结果呢?可能没有办法从for语句中删除阻塞型的PHP API。不同于之前的对同一个文件多次写入不同数据,考虑另一种情况,利用for语句将同一段数据写入任意数目的不同文件(比如,一堆日志文件):

for($i=0; $i < 3; ++$i){
  $fp = fopen($str+$i,'w');
  fwrite($fp,$data);
  fclose($fp);
}

不经过重构直接转换Node.js,for语句由回调函数链替换:

fs.open(‘fp.txt’,’w’,0666,function(error,fp){
  fs.write(fp,data,null,’utf-8’,function(){
    fs.close(fp,function(error){
      fs.open(fp,data,null,’utf-8’,function(){
        fs.close(fp,function(){
          //and on and on for a total of "n" times
        });
      });
    });
  });
});

怎样避免回调函数链呢?需要单独对每个文件进行写操作。没有特定的非阻塞型Node.js API可以在一个回调函数中对多个文件惊醒写操作。如果需要对每个文件爱你单独进行写操作,就一定会多次调用Node.js API,每次调用都有自己的回调函数,而且嵌套在前一次调用的回调函数中。

在下一章中,会介绍一种更复杂的转换方式来解决这种问题。这种转换方式包含PHP continuation(时序),一种在PHP代码(甚至PHP4代码)中模拟回调的方式,但是不支持PHP5中添加的有助于支持Node.js风格回调的新语言特性(比如,lambda和匿名函数)。

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

    0

  • 没用

    0

  • 开心

    0

  • 愤怒

    0

  • 可怜

    0

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

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

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

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

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

我要说说
网上宾友点评