为 PHP 开发人员介绍 Node.JS #1 : 事件驱动与意大利面

身为PHP开发人员,Node.js中最难弄清的部分就是异步。它是一种全新的代码方式。初步学习后,基于事件驱动的编程会为PHP开发人员创造更多的可能。我将为你解释他如何实现,不过首先来了解意大利面。

身为PHP开发人员,Node.js中最难弄清的部分就是异步。它是一种全新的代码方式。初步学习后,基于事件驱动的编程会为PHP开发人员创造更多的可能。我将为你解释他如何实现,不过首先来了解意大利面。

意大利面配方

我喜欢意大利面,但它不容易做好,因而我有个格言:不要吃不靠谱的人做的意大利面。就算你靠谱,我也给你一个简单的配方,不会把面搞糟。这样我们就能坐下来一起吃了。

傅小黑

傅小黑
翻译于 3年前

3人顶

 翻译的不错哦!

这个配方就是意大利面条+新鲜番茄酱。平底锅加大量水,少许盐 – 10分钟水沸腾。同时,削皮和剁开新鲜番茄。削皮、切碎洋葱、胡萝卜和西芹菜。这些叫做蔬菜调料(mirepoix),或者意大利语的soffritto。待水煮沸,放意大利面条继续煮。同时,另一锅加一点橄榄油热蔬菜调料几分钟。然后加入番茄,香草料,盐和胡椒,搅拌、番茄至多热5分钟,因为之后会产生一定的酸味。时不时尝一下面条直到有嚼劲时出水,到酱板放干,再混合番茄酱和切碎的蔬菜调料里,带一点茴香和帕尔马奶酪。就这样啦!(法语:Voilà)

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

同步编程

看了意大利面制作方法想不挨饿,就继续下一步:怎么让你电脑做意大利面?PHP会这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
// time is 0
$pastaPan new Pan();//一个锅
$water new Water(); //一些水
$pastaPan->fill($water); //锅里加水
$pastaPan->warm($duration = 10); //热10分钟
// now time is 10
$pastaPan->fill(new Spaghetti()); //加面条
$pastaPan->warm($duration = 8); //热8分钟
// now time is 18
$pastaPan->remove($water); //取面条
$saucePan new Pan(); //又一个锅
$saucePan->fill(new OliveOil()); //一点橄榄油
$saucePan->warm($duration = 2); //热2分钟
// now time is 20
$saucePan->fill(MirepoixFactory::create($withGarlic = true)); //加带大蒜的蔬菜调料
$saucePan->warm($duration = 5); //热5分钟
// now time is 25
$saucePan->fill(TomatoFactory::create()); //加西红柿
$saucepan->warm($duration = 4); //热4分钟
// now time is 29
$plate new Plate(); //一个酱板
$plate->addContentsOf($pastaPan); //加面条
$plate->addContentsOf($saucePan); //加番茄和蔬菜
$plate->serve('Voilà'); //拌面,做好

这段程序最大的问题是做饭花了29分钟,而且面条在酱板上等番茄酱10分钟,都冷了。冷面到没法儿吃的。当我做面条的时候,煮沸水的同时切蔬菜。再读读菜谱:一些标识词语“in the meantime”(同时),“when it boils”(煮水的时候),“taste regularly”(时不时尝一下)。PHP程序没做到这些,做出来的面条不能吃的。

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

异步编程

让我们教会PHP做面条,最好用的模式是事件驱动编程。程序依赖一个中心事件循环类(EventLoop)。事件循环类循环自增内部标识(tick)。其他服务为特定的标识添加回调。当所有回调执行,循环结束。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
<?php
class EventLoop
{
  protected $tick = 0;
  protected $callbacksForTick array(); //记录tick的回调函数
  public function start()
  {
    while ($this->callbacksForTick) { //没有回调时才停止
      $this->tick++; //自增tick
      $this->executeCallbacks(); //执行回调
    }
  }
  public function executeCallbacks()
  {
    echo "Tick is " $this->tick . "\n";
    if (!isset($this->callbacksForTick[$this->tick])) {
      return// 没有回调,就return
    }
    foreach ($this->callbacksForTick[$this->tick] as $callback) {
      call_user_func($callback$this); //给回调传递自身作为参数
    }
    // 删除已经执行的回调
    unset($this->callbacksForTick[$this->tick]);
  }
  public function executeLater($delay$callback)
  {
    $this->callbacksForTick[$this->tick + $delay] []= $callback;//推迟执行回调
  }
}

利用事件循环类,一种新的类可以创建:异步服务,如异步的锅:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php
class AsynchronousPan extends Pan
{
  protected $eventLoop;
  public function __construct(EventLoop $eventLoop)
  {
    $this->eventLoop = $eventLoop;
  }
  public function warm($duration$callback)
  {
    $this->eventLoop->executeLater($duration$callback);//水热了后回调
  }
}

结合事件循环和异步的锅,可以传递回调实现异步的热锅过程:

1
2
3
4
5
6
7
8
<?php
$eventLoop new EventLoop();
$pan new AsynchronousPan($eventLoop);
echo "Starting to warm\n";
$pan->warm(10, function() {
  echo "Now it's cooked\n";
});
$eventLoop->start();

执行操作,你可以看到如下输出:

1
2
3
4
5
6
7
8
9
10
11
12
Starting to warm
Tick is 1
Tick is 2
Tick is 3
Tick is 4
Tick is 5
Tick is 6
Tick is 7
Tick is 8
Tick is 9
Tick is 10
Now it's cooked
傅小黑

傅小黑
翻译于 3年前

1人顶

 翻译的不错哦!

异步做面条

做面条已经万事俱备。现在,做番茄酱和煮面可以同时了吗?煮面18分钟,做番茄酱11分钟,那么程序需要等7分钟来做番茄酱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
$eventLoop new EventLoop(); //调用事件循环类
$plate new Plate();
$pastaPan new AsynchronousPan($eventLoop);
$water new Water();
$pastaPan->fill($water);//加水
echo "pastaPan: Starting to boil water\n";
$pastaPan->warm($duration = 10, function() use ($pastaPan$plate$water) {
  echo "pastaPan: Water is boiling\n";//水开了
  $pastaPan->fill(new Spaghetti()); //加面条
  echo "pastaPan: Starting to boil spaghetti\n";
  $pastaPan->warm($duration = 8, function() use ($pastaPan$plate$water) { //煮面条
    echo "pastaPan: Spaghetti is ready\n";
    $pastaPan->remove($water);//捞面条
    $plate->addContentsOf($pastaPan);//面条放到酱板上
  });
});
$eventLoop->executeLater($delay = 7, function() use ($plate$eventLoop) {
  $saucePan new AsynchronousPan($eventLoop); //番茄酱锅
  $saucePan->fill(new OliveOil());//加橄榄油
  echo "saucePan: Starting to warm olive oil\n";
  $saucePan->warm($duration = 2, function() use($saucePan$plate) {
    echo "saucePan: Olive oil is warm\n";
    $saucePan->fill(MirepoixFactory::create($withGarlic = true));//热2分钟后,加蔬菜调料
    echo "saucePan: Starting to cook the Mirepoix\n";
    $saucePan->warm($duration = 5, function() use($saucePan$plate) {
      echo "saucePan: Mirepoix is ready to welcome tomato\n";//热5分钟后加番茄
      $saucePan->fill(TomatoFactory::create());
      echo "saucePan: Starting to cook tomato\n";
      $saucePan->warm($duration = 4, function() use($saucePan$plate) {
        echo "saucePan: Tomato sauce is ready\n";
        $plate->addContentsOf($saucePan);//热4分钟,番茄酱做好拌面
      });
    });
  });
});
$eventLoop->start();//回调都写好了,开始做面
$plate->serve('Voilà');//最后拌面

PHP执行后,结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
pastaPan: Starting to boil water
Tick is 1
...
Tick is 7
saucePan: Starting to warm olive oil
Tick is 8
Tick is 9
saucePan: Olive oil is warm
saucePan: Starting to cook the Mirepoix
Tick is 10
pastaPan: Water is boiling
pastaPan: Starting to boil spaghetti
Tick is 11
....
Tick is 14
saucePan: Mirepoix is ready to welcome tomato
saucePan: Starting to cook tomato
Tick is 15
...
Tick is 18
pastaPan: Spaghetti is ready
saucePan: Tomato sauce is ready

赞!所有事情18分钟内做好咯!

注意:你对比前后的代码,会发现明显的差异。前面的代码垂直对齐的,你从上到下阅读就能明白流程。后面代码中的缩进告诉你执行操作后执行另一个。为了继续以下的内容,你必须从左向右阅读。事件驱动的编程中,缩进意味着顺序。

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

回调函数之间是同步的

还有一个问题。拌面是在最后,回调执行结束。但是如果还有后续的回调操作,比如洗盘子?那拌面可能被推迟,面条都冷了。这不能发生,都逆天了!

所以拌面不能在事件循环结束后,即循环执行$eventLoop->start()的后面没有代码。但怎样同步酱板里面条和番茄酱呢?他们可是在不同的队列里!

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

你可等事件tick到了18分钟再拌面,但是这就像赌博(猜tick到了没)。异步服务包括这个例子,都是基于时间的。猜时间的情况很少。大多数的异步回调都由第三方服务的返回触发,如查询数据库,打开一个文件,获得HTTP返回。你真不知道何时会执行回调。因此你必须依赖新技术“再同步”(resynchronization)。

如意大利面的例子,很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php
class PlateOfSpaghettiWithSauce extends Plate
{
  protected $hasSpaghetti = false;
  protected $hasSauce = false;
  public function addContentsOf(Pan $pan)
  {
    parent::addContentsOf($pan);
    if ($pan->contains('Spaghetti')) {
      $this->hasSpaghetti = true;
    }
    if ($pan->contains('Tomato')) {
      $this->hasSauce = true;
    }
    if ($this->hasSpaghetti && $this->hasSauce) {//当组分完成,可以拌面,这是同步操作
      $this->serve('Voilà');
    }
  }
}

酱板需要知道它装什么内容,并检查是否已经都获得了。这个例子无所谓,但是你必须记住你是在事件驱动中同步操作,而不是运行时环境。

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

还只是一道菜

前端代码花了29分钟做面条,后面的花了18分钟。不过这还只是一道菜——编程术语里的一个线程。你可以加一个线程让做面条更快,但是具体事情不会被加速:如果你用喷火器热番茄,不会更快熟了,而是直接焦了。

重新描述一下:这不是并行编程。这些代码还是按顺序执行,不是事件循环看起来像别的什么在操作,这是错觉。这就是把一些操作打包在短时间执行。就像CPU使用更少的周期去等待阻塞操作,更多的周期用于运算。事件驱动编程让你更有效利用CPU。

顺序编程里,浪费的CPU周期被并行线程挽回。这就是为什么通过mod_php使用PHP+Apache的时候,要定义并发线程数。不过优缺点,每次你创建线程去使用别的线程浪费的CPU,会占用内存。归结起来:为了通过事件驱动编程提高CPU效率,顺序执行的程序需要更多内存。内存是Web服务器最金贵的部分。(即更多的菜更占内存)

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

Node.js?

相比PHP,Node.js怎么样呢?同样的方式比较第一段和最后一段代码。Node.js提倡事件驱动的编程,并且简化了它。首先Node.js基于JavaScript,原生支持事件系统。然后,Node.js提供了EventLoop对象,你可以在主脚本结束时调用它。最后Node.js还原生提供大多利用了异步的I/O阻塞操作。

比如Node.js删除磁盘文件,代码如下:

1
2
3
4
5
6
7
var fs = require('fs');
fs.unlink('/tmp/hello'function (err) {
  if (err) throw err;
  console.log('successfully deleted /tmp/hello');
});
// more code
console.log('deletion script');

Node.js从上到下执行脚本。unlink()函数告诉磁盘寻找某个特定文件,删除它并反馈已完成。同时Node.js可以执行别的代码。输出”delete message”会比实际删除先出现。脚本执行到最后,事件循环开始。程序准备接受文件系统返回的删除操作信号,而这触发匿名函数,打印出”successfully deleted”。

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

JavaScript也有PHP闭包类似的语法糖。因为作用域,JavaScript声明匿名函数不需要使用use关键字,及JavaScript会记住执行函数之间的状态。因此,PHP异步调用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
$plate new Plate();
$pastaPan new AsynchronousPan($eventLoop);
$water new Water();
$pastaPan->fill($water);
echo "pastaPan: Starting to boil water\n";
$pastaPan->warm($duration = 10, function() use ($pastaPan$plate$water) {
  echo "pastaPan: Water is boiling\n";
  $pastaPan->fill(new Spaghetti());
  echo "pastaPan: Starting to boil spaghetti\n";
  $pastaPan->warm($duration = 8, function() use ($pastaPan$plate$water) {
    echo "pastaPan: Spaghetti is ready\n";
    $pastaPan->remove($water);
    $plate->addContentsOf($pastaPan);
  });
});

转换成JavaScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
plate = new Plate();
pastaPan = new AsynchronousPan();
water = new Water();
pastaPan.fill(water);
console.log('pastaPan: Starting to boil water');
pastaPan.warm(duration = 10, function() {
  console.log('pastaPan: Water is boiling');
  pastaPan.fill(new Spaghetti());
  console.log('pastaPan: Starting to boil spaghetti');
  pastaPan.warm(duration = 8, function() {
    console.log('pastaPan: Spaghetti is ready');
    pastaPan.remove(water);
    plate.addContentsOf(pastaPan);
  });
});

最后,Node.js可以写动态服务器,但PHP需要服务器(如Apache)去处理原始HTTP请求。这意味着Node.js服务器常驻内存,只需编译一次。对比mod_php,加载整个php运行时,每次HTTP请求都引入所有脚本,Node.js有很大提升。

注意:PHP注意JavaScript一个重要的陷阱,邪恶的“this”关键字。我在之前的博客中提到。

傅小黑

傅小黑
翻译于 3年前

0人顶

 翻译的不错哦!

结论

总体而言,Node.js 的速度比 PHP 快,而且占用内存更少。但是一旦服务器的代码量不断的增长,Node 的程序也变得异常复杂。因为你需要手工的进行同步。因此 Node 适合编制一些小或者中型规模的服务器端代码,而且逻辑比较简单的,例如 REST API 就很适合。

对我来说,Node 的事件循环返回的 true 值,是新的异步实现的数据查询、文件操作、HTTP 请求等等。当你意识到其他实现基本是在浪费你的 CPU 周期时,这是很有价值的。

现在你已经意识到了 Node 的最大特色,我希望你去试一试。我也希望你可以使用我的开源调味料来轻松的烹制符合自己口味的意大利面。

http://www.oschina.net/translate/nodejs-for-php-programmers-1-event-driven-pro
为 PHP 开发人员介绍 Node.JS #1 : 事件驱动与意大利面
标签:             

发表评论

电子邮件地址不会被公开。 必填项已用*标注

*