node.js events

Javascript 是一個事件驅動的語言. 這也是讓 node.js 這麼受矚目最重要的因素之一. Javascript 運作起來就像人一樣, 舉例來說下面有五件事情需要去達成, 分別是

1. Make cup noodles( takes 3 mins )
2. Answer a phone call( takes 1 min )
3. Go to toilet( takes 20 secs )
4. Eat the noodles( takes 5 mins )
5. Throw the cup to trash can( takes 3 secs )

正常人會先泡泡麵, 在等水煮開的時候去接電話和上廁所. 當泡麵主好後把他給吃了然後丟進垃圾桶. 但是非事件觸發的語言像是 PHP 要達成這個目標就比較困難一點了.

// 開始泡泡麵
make_cup_noodles();

// 在等泡麵泡好的時候電話響了, 但是我還是必須等麵泡完了才能接, 於是我變錯過了這通電話
answer_a_phone_call();

// 這時我突然想尿尿, 但我還是必須等面泡完, 於是我便尿褲子了...
go_to_toilet();

// 麵好了我穿著滿是尿的褲子吃麵
eat_the_noodles();

// 吃完了我穿著滿是尿的褲子丟掉空碗
throw_the_cup_to_trash_can();

如果是用像 javascript 這種事件驅動的語言就不會是這個樣子了. 但是上面的範例還是行不通.

steps.js | 包含了上面需要做的動作
module.exports = {

  cup_noodles : '這是還沒煮的泡麵',

  make_cup_noodles : function( callback ){
    var self = this;

    console.log( '開始泡泡麵' );

    // 模擬需要長時間的函式
    setTimeout( function(){
      self.cup_noodles = self.cup_noodles === '這是還沒煮的泡麵' ?
        '泡麵煮好了' : self.cup_noodles;

      console.log( self.cup_noodles );

      callback && callback.call( this );
    }, 3000 );
  },

  answer_a_phone_call : function(){
    var action = this.ringing === '電話響了' ?
        '我接了這通電話' : '我錯過了這通電話';

      console.log( action );
  },

  go_to_toilet : function(){
    this.pants = 'Off';
  },

  eat_the_noodles : function( callback ){
    var self = this;

    setTimeout( function(){
      self.cup_noodles = self.cup_noodles === '泡麵煮好了' ?
        '我吃完了' : '我啥都沒吃到...'

      console.log( self.cup_noodles );

      callback && callback.call( this );
    }, 5000 );
  },

  throw_the_cup_to_trash_can : function(){
    var self = this;

    setTimeout( function(){
      self.cup_noodles = self.cup_noodles === '我吃完了' ?
        '我吃完泡麵並把他丟進垃圾桶' : '泡麵浪費了'

      console.log( self.cup_noodles );
    }, 30 );
  }
};
pee.js | 尿尿的動作
module.exports = {

  action : '',

  pants : 'On',

  explode : function( callback ){
    var self = this;

    callback && callback.call( this );

    this.action = this.pants === 'On' ?
      '尿在褲子上' : '尿尿中...';

    console.log( this.action );

    setTimeout( function(){
      self.pants = self.action === '尿在褲子上' ?
        '我尿褲子了' : '我尿完了';

      console.log( self.pants );
    }, 500 );
  }
};
phone.js | 電話響了的動作
module.exports = {

  ringing : '',

  ring : function( callback ){
    var self = this;

    this.ringing = '電話響了...';
    console.log( this.ringing );

    callback && callback.call( this );

    setTimeout( function(){
      self.ringing = '電話不響了';
      console.log( self.ringing );
    }, 1000 );
  }
};
wrong.js | 錯誤示範
var steps = require( './steps' ),
    phone = require( './phone' ),
    pee = require( './pee' );

// 開始泡泡麵
steps.make_cup_noodles();

// ok 我現在是事件觸發模式所以我不用等泡麵泡好才能作其他事
// 但是我們現在多了一些新問題, 在同一個 scope 的函式幾乎是在同一時間觸發
phone.ring();

// 意思就是我有可能沒辦法接到這通電話
steps.answer_a_phone_call();

// 想尿尿
pee.explode();

// 還是有問題
steps.go_to_toilet();

// 我想吃泡麵但是已經被丟掉了
steps.eat_the_noodles();

// 一樣的錯誤在這裡發生, 還在煮的泡麵就這樣被丟掉了
// 所以到最後我一樣尿在褲子上也沒接到電話然後一樣沒吃到泡麵
steps.throw_the_cup_to_trash_can();
結果
開始泡泡麵
電話響了...
我錯過了這通電話
尿在褲子上
泡麵浪費了
我尿褲子了
電話不響了
泡麵浪費了
我啥都沒吃到...

為了確保一件事在另一件事之後發生, 記得要在 callback 裡面去呼叫函式.

right.js | 正確示範
var steps = require( './steps' ),
    phone = require( './phone' ),
    pee = require( './pee' );

steps.make_cup_noodles( function(){
  steps.eat_the_noodles( function(){
    steps.throw_the_cup_to_trash_can();
  });
});

phone.ring( steps.answer_a_phone_call );

pee.explode( steps.go_to_toilet );
結果
開始泡泡麵
電話響了...
我接了這通電話
尿尿中...
我尿完了
電話不響了
泡麵煮好了
我吃完了
我吃完泡麵並把他丟進垃圾桶
希望上面的範例能讓你了解事件驅動的語言是如何運作的以及為何 node.js 會這麼快 Hope the above example shows you how a event driven language works and why node.js is so fast

node.js Events

在這裡我不是要將 node.js docevents 的部分一一說明. 相對的我想解釋為什麼以及要如何去使用這個模組.

從上一個範例裡我們知道如果要確認一件事發生在另一件事之後, 我們必須要將他寫在 callback 裡面. 但如果動作很多且複雜時我們的程式常常就會變成像下面的巢狀結構

do_a( function(){
  do_b( function(){
    do_c( function(){
      do_d( function(){
        ...
      });
    });
  });
});

這不但不方便閱讀而且也不好維護. 更不用說要將這種程式模組化了. 這時候用 node.js events 模組就很好用了. 使用 EventEmitter 我們可以將上面的範例改成下面這樣

event.js
var event = require( 'events' ).EventEmitter;

module.exports = new event;
do_a.js
var event = require( './event' );

module.exports = function(){
  console.log( '呼叫 do_a' );
  event.emit( 'do_a' );
};
do_b.js
var event = require( './event' );

module.exports = function(){
  event.on( 'do_a', function(){
    console.log( '呼叫 do_b' );
    event.emit( 'do_b' );
  });
};
do_c.js
var event = require( './event' );

module.exports = function(){
  event.on( 'do_b', function(){
    console.log( '呼叫 do_c' );
    event.emit( 'do_c' );
  });
};
do_d.js
var event = require( './event' );

module.exports = function(){
  event.on( 'do_c', function(){
    console.log( '呼叫 do_d' );
    event.emit( 'do_d' );
  });
};
run.js
var event, dos;

event = require( './event' );
todos = [ './do_d', './do_c', './do_b', './do_a' ];

event.on( 'do_a', function(){
  console.log( '我可以在外部加入需要做的事情到 do_a 裡面' );
});

event.on( 'do_b', function(){
  console.log( '我可以在外部加入需要做的事情到 do_b 裡面' );
});

event.on( 'do_c', function(){
  console.log( '我可以在外部加入需要做的事情到 do_c 裡面' );
});

event.on( 'do_d', function(){
  console.log( '我可以在外部加入需要做的事情到 do_d 裡面' );
});

todos.forEach( function( name ){
  require( name )();
});
Result of calling run.js
呼叫 do_a
我可以在外部加入需要做的事情到 do_a 裡面
呼叫 do_b
我可以在外部加入需要做的事情到 do_b 裡面
呼叫 do_c
我可以在外部加入需要做的事情到 do_c 裡面
呼叫 do_d
我可以在外部加入需要做的事情到 do_d 裡面

從上面的結果看來你可能會覺得怎麼好像變複雜了. 沒錯, 對小專案來說是如此. 但是對大型專案來說我們可以用這種方法來把程式碼作切分而又確保他發生的順序. 有者這個模組我們可以更輕鬆的打造更有彈性的應用程式.


Related posts