首頁  >  文章  >  web前端  >  Node.js中使用事件發射器模式實作事件綁定詳解_node.js

Node.js中使用事件發射器模式實作事件綁定詳解_node.js

WBOY
WBOY原創
2016-05-16 16:39:321445瀏覽

在Node裡,很多物件都會發射事件。例如,一個TCP伺服器,每當有客戶端請求連線就會發射「connect」事件,又例如,每當讀取一整塊數據,檔案系統就會發射一個「data」事件。這些物件在Node裡被稱為事件發射器(event emitter)。事件發射器允許程式設計師訂閱他們感興趣的事件,並將回調函數綁定到相關的事件上,這樣每當事件發射器發射事件時回呼函數就會被呼叫。發布/訂閱模式非常類似傳統的GUI模式,例如按鈕被點擊時程式就會收到對應的通知。使用這種模式,服務端程式可以在一些事件發生時作出反應,例如有客戶端連接,socket上有可用數據,或是檔案關閉的時候。

還可以創建自己的事件發射器,事實上,Node專門提供了一個EventEmitter偽類,可以把它當作基類來創建自己的事件發射器。

理解回呼模式

非同步程式設計不使用函數傳回值來表示函數呼叫的結束,而是採用後繼傳遞風格。

「後繼傳遞風格」(CPS:Continuation-passing style)是一種程式設計風格,流程控制被明確傳遞給下一步操作…

CPS風格的函數會接受一個函數作為額外參數,這個函數用來明確指出程式控制的下個流程,當CPS函數計算出它的“返回值”,它就會呼叫那個代表了程式下個流程的函數,並將CPS函數的「返回值」作為其參數。

出自維基百科——http://en.wikipedia.org/wiki/Continuation-passing_style

這種程式設計風格裡,每個函數在執行結束後都會呼叫一個回呼函數,這樣程式就可以繼續運作。後面你會明白,JavaScript非常適合這種程式設計風格,以下是個Node下將檔案載入到記憶體的範例:

複製程式碼 程式碼如下:

var fs = require('fs');

fs.readFile('/etc/passwd', function(err, fileContent) {

    if (err) {

        throw err;

    }

    console.log('file content', fileContent.toString());

});

這個例子裡,你傳遞了一個內聯匿名函數作為fs.readFile的第二個參數,其實這就是在使用CPS編程,因為你把程式執行的後續流程交給了那個回調函數。

如你所見,回呼函數的第一個參數是個錯誤對象,如果程式發生錯誤,這個參數將會是一個Error類別的實例,這是Node裡CPS程式設計的一個常見模式。

瞭解事件發射器模式

標準回呼模式裡,把一個函數當作參數傳遞給將被執行的函數,這種模式在客戶端需要在函數完成後被通知的場景下運作的很好。但是如果函數的執行過程中發生了多個事件或事件重複發生了多次,這種模式就不太適合了。例如,你想在socket每次收到可用資料時得到通知,這種場景你會發現標準回呼模式不太好用,這時事件發射器模式就派上用場了,你可以用一套標準接口來清晰的分離事件產生器和事件監聽器。

使用事件產生器模式時,會涉及兩個或多個物件-事件發射器和一個或多個事件監聽器。

事件發射器,顧名思義,是個可以產生事件的物件。而事件監聽器則是綁定到事件發射器上的程式碼,用來監聽特定類型的事件,就像下面的例子:

複製程式碼 程式碼如下:

var req = http.request(options, function(response) {

    response.on("data", function(data) {

        console.log("some data from the response", data);

    });

    response.on("end", function() {

         console.log("response ended");

    });

});

req.end();

這段程式碼示範了用Node的 http.request API(見後面章節)建立一個HTTP請求來存取遠端HTTP伺服器時的兩個必要步驟。第一行採用了「後繼傳遞風格」(CPS:Continuation-passing style),傳遞了一個當HTTP回應時會被呼叫的內聯函數。 HTTP請求API在這裡使用CPS是因為程式需要在http.request函數執行完畢後才繼續執行後續操作。

當http.request執行完畢,就會調用那個匿名回調函數,然後將HTTP響應對像作為參數傳遞給它,這個HTTP響應對像是個事件發射器,根據Node文檔,它可以發射包括data,end在內的許多事件,你註冊的那些回呼函數會在每次事件發生時被呼叫。

作為一條經驗,當你需要在請求的操作完成後重新獲取執行權時使用CPS模式,以及當事件可以發生多次時使用事件發射器模式。

理解事件類型

被發射的事件都有一個用字串表示的類型,前面的例子包含「data」和「end」兩個事件類型,它們是由事件發射器來定義的任意字串,不過約定俗成的是,事件類型通常都由不包含空字元的小寫單字組成。

不能用程式碼來推斷事件發射器能產生哪些類型的事件,因為事件發射器API並沒有內省機制,因此你使用的API應該有文件來表示它能發射那些類型的事件。

一旦事件發生,事件發射器就會呼叫跟事件相關的監聽器,並將相關資料作為參數傳遞給監聽器。在前面http.request那個例子裡,「data」事件回調函數接受一個data物件作為它第一個也是唯一的參數,而「end」不接受任何數據,這些參數作為API契約的一部分也是由API的作者主觀定義的,這些回呼函數的參數簽章也會在每個事件發射器的API文件裡有說明。

事件發射器雖然是個為所有類型事件服務的接口,不過「error」事件是Node裡的一個特殊實作。 Node裡的大多數事件發射器都會在程式發生錯誤時產生「error」事件,如果程式沒有監聽某個事件發射器的「error」事件,事件發射器將會注意到並在錯誤發生時向上拋出一個未捕獲異常。

你可以在Node PERL裡運行下面的程式碼來測試下效果,它模擬了一個能產生兩種事件的事件發射器:

複製程式碼 程式碼如下:

var em = new (require('events').EventEmitter)();

em.emit('event1');

em.emit('error', new Error('My mistake'));

你將會看到下面的輸出:

複製程式碼 程式碼如下:

var em = new (require('events').EventEmitter)();

undefined

> em.emit('event1');

false

> em.emit('error', new Error('My mistake'));

Error: My mistake

at repl:1:18

at REPLServer.eval (repl.js:80:21)

at repl.js:190:20

at REPLServer.eval (repl.js:87:5)

at Interface. (repl.js:182:12)

at Interface.emit (events.js:67:17)

at Interface._onLine (readline.js:162:10)

at Interface._line (readline.js:426:8)

at Interface._ttyWrite (readline.js:603:14)

at ReadStream. (readline.js:82:12)

>

程式碼第2行,隨便發射了一個叫做「event1」的事件,沒有任何效果,但是當發射「error」事件時,錯誤被拋出到堆疊。如果程式不是運行在PERL命令列環境裡,程式將會因為未捕獲的異常而崩潰。

使用事件發射器API

任何實作了事件發射器模式的物件(例如TCP Socket,HTTP 請求等)都實作了下面的一組方法:

複製程式碼 程式碼如下:

.addListener和.on —— 為指定類型的事件新增事件監聽器
.once —— 為指定類型的事件綁定一個僅執行一次的事件監聽器
.removeEventListener —— 刪除綁定到指定事件上的某個監聽器
.removeAllEventListener —— 刪除所有綁定到指定事件上的監聽器

下面我們具體介紹它們。

使用.addListener()或.on()綁定回呼函數

透過指定事件類型和回呼函數,你可以註冊當事件發生時被執行的操作。例如,檔案讀取資料流時如果有可用的資料區塊,就會發射一個「data」事件,下面程式碼展示如何透過傳入一個回呼函數來讓程式告訴你發生了data事件。

複製程式碼 程式碼如下:

function receiveData(data) {

   console.log("got data from file read stream: %j", data);

}

readStream.addListener(“data”, receiveData);

你也可以使用.on,它只是.addListener的簡寫方式,下面的程式碼和上面的是一樣的:

複製程式碼 程式碼如下:

function receiveData(data) {

   console.log("got data from file read stream: %j", data);

}
readStream.on(“data”, receiveData);

前面程式碼,使用事先定義的一個的命名函數作為回呼函數,你也可以使用一個內聯匿名函數來簡化程式碼:

複製程式碼 程式碼如下:

readStream.on("data", function(data) {

   console.log("got data from file read stream: %j", data);

});


前面說過,傳遞給回調函數的參數個數和簽名依賴於具體的事件發射器對象和事件類型,它們並不是被標準化的,“data”事件可能傳遞的是一個數據緩衝對象,“error”事件傳遞一個錯誤對象,資料流的「end」事件不向事件監聽器傳遞任何資料。

綁定多個事件監聽器

事件發射器模式允許多個事件監聽器監聽同一個事件發射器的相同事件類型,例如:

複製程式碼 程式碼如下:

I have some data here.

I have some data here too.

事件發射器負責依照監聽器的註冊順序呼叫指定事件類型上綁定的所有監聽器,也就是說:

1.當事件發生後事件監聽器可能不會被立刻調用,也許會有其它事件監聽器在它之前被調用。
2.異常被拋出到堆疊是不正常的行為,可能是因為程式碼裡有bug,當事件被發射時,如果有一個事件監聽器在被呼叫時拋出了異常,可能會導致一些事件監聽器永遠不會被呼叫。在這種情況下,事件發射器會捕獲到異常,也許還會處理它。

看下面這個範例:

複製程式碼 程式碼如下:

readStream.on("data", function(data) {

   throw new Error("Something wrong has happened");

});

readStream.on("data", function(data) {

   console.log('I have some data here too.');

});

因為第一個監聽器拋出了異常,因此第二個監聽器不會被呼叫。

用.removeListener()從事件發射器移除一個事件監聽器

如果當你不再關心一個物件的某個事件時,你可以透過指定事件類型和回呼函數來取消已註冊的事件監聽器,像這樣:

複製程式碼 程式碼如下:

function receiveData(data) {

    console.log("got data from file read stream: %j", data);

}

readStream.on("data", receiveData);

// ...

readStream.removeListener("data", receiveData);

這個例子裡,最後一行把一個可能在將來被隨時呼叫的事件監聽器從事件發射器物件移除了。

為了刪除監聽器,你必須為回呼函數命名,因為在新增和刪除的時候需要回呼函數的名字。

使用.once()讓回呼函數執行最多一次

如果你想監聽一個最多執行一次的事件,或者只對某個事件發生的第一次感興趣,可以用.once()函數:

複製程式碼 程式碼如下:

function receiveData(data) {

    console.log("got data from file read stream: %j", data);

}

readStream.once("data", receiveData);

上面的程式碼,receiveData函數只會被呼叫一次。如果readStream物件發射了data事件,receiveData回呼函數將會且只會被觸發一次。

它其實只是個方便方法,因為很簡單的就能實現它,像這樣:

複製程式碼 程式碼如下:

var EventEmitter = require("events").EventEmitter;

EventEmitter.prototype.once = function(type, callback) {

   var that = this;

   this.on(type, function listener() {

      that.removeListener(type, listener);

      callback.apply(that, arguments);

   });

};

上面程式碼裡,你重新設定了EventEmitter.prototype.once函數,同時也重新定義了每個繼承自EventEmitter的所有物件的once函數。程式碼只是簡單的使用.on()方法,一旦收到了事件,就用.removeEventListener()取消回呼函數的註冊,並呼叫原來的回呼函數。

注意:前面程式碼裡使用了function.apply()方法,它接受一個物件並把它當作內含的this變量,以及一個參數數組。前面例子裡,透過事件發射器把未修改過的參數陣列透明地傳遞給回呼函數。

用.removeAllListeners()從事件發射器移除所有事件監聽器

你可以像下面一樣從事件發射器移除所有註冊到指定事件類型上的所有監聽器:

複製程式碼 程式碼如下:

emitter.removeAllListeners(type);

例如,你可以這樣取消所有行程中斷訊號的監聽器:

複製程式碼 程式碼如下:

process.removeAllListeners("SIGTERM");

注意:作為一條經驗,推薦你只在確切知道刪除了什麼內容時才使用這個函數,否則,應該讓應用程式其它部分來刪除事件監聽器集合,或者也可以讓程式的那些部分自己負責移除監聽器。但不管怎樣,在某些罕見的場景下,這個函數還是很有用的,例如當你準備有序的關閉一個事件發射器或關閉整個進程的時候。

建立事件發射器

事件發射器用一個很棒的方式讓程式設計介面變得更通用,在一個常見易懂的程式模式裡,客戶端直接呼叫各種函數,而在事件發射器模式中,客戶端被綁定到各種事件上,這會讓你的程式變得更靈活。 (譯者註:這句不太自信,貼出原文:The event emitter provides a great way of making a programming interface more generic. When you use a common understood pattern, clients bind to events instead of invoking functions, making your program more flexible.)

此外,透過使用事件發射器,你還可以獲得許多特性,例如在同一事件上綁定多個互不相關的監聽器。

繼承從Node事件發射器

如果你對Node的事件發射器模式感興趣,並打算用到自己的應用程式裡,你可以透過繼承EventEmitter來建立一個偽類:

複製程式碼 程式碼如下:

util = require('util');

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

// 這是MyClass的建構子:

var MyClass = function() {

}

util.inherits(MyClass, EventEmitter);

注意:util.inherits建立了MyClass的原形鏈,讓你的MyClass實例可以使用EventEmitter的原形方法。

發射事件

透過繼承自EventEmitter,MyClass可以這樣發射事件了:

複製程式碼 程式碼如下:

MyClass.prototype.someMethod = function() {

    this.emit("custom event", "argument 1", "argument 2");

};

上面的程式碼,當someMethond方法被MyClass的實例呼叫時,就會發射一個叫做“cuteom event”的事件,這個事件還會發射兩個字串作為資料:“argument 1”和“argument 2” ,它們將會作為參數傳遞給事件監聽器。

MyClass實例的客戶端可以像這樣監聽「custom event」事件:

複製程式碼 程式碼如下:

var myInstance = new MyClass();

myInstance.on('custom event', function(str1, str2) {

    console.log('got a custom event with the str1 %s and str2 %s!', str1, str2);

});

再例如,你可以這樣建立一個每秒發射一次「tick」事件的Ticker類別:

複製程式碼 程式碼如下:

var util = require('util'),

EventEmitter = require('events').EventEmitter;

var Ticker = function() {

    var self = this;

    setInterval(function() {

        self.emit('tick');

    }, 1000);

};

util.inherits(Ticker, EventEmitter);

用Ticker類別的客戶端可以展示如何使用Ticker類別和監聽「tick」事件,

複製程式碼 程式碼如下:

var ticker = new Ticker();

ticker.on("tick", function() {

    console.log("tick");

});

小結

事件發射器模式是種可重入模式(recurrent pattern),可以用它將事件發射器物件從一組特定事件的程式碼中解耦合。

可以用event_emitter.on()來為特定類型的事件註冊監聽器,並用event_emitter.removeListener()來取消註冊。

也可以透過繼承EventEmitter和簡單的使用.emit()函數來建立自己的事件發射器。

陳述:
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn