【深刻淺出Node.js系列四】Node.js的事件機制

#0 系列目錄#html

Node核心思想:  1.非阻塞;  2.單線程;  3.事件驅動;前端

在目前的web應用中,客戶端和服務器端之間有些交互能夠認爲是基於事件的,那麼AJAX就是頁面及時響應的關鍵。每次發送一個請求時(無論請求的數據多麼小),都會在網絡裏走一個來回。服務器必須針對這個請求做出響應,一般是開闢一個新的進程。那麼越多用戶訪問這個頁面,所發起的請求個數就會愈來愈多,就會出現內存溢出、邏輯交錯帶來的衝突、網絡癱瘓、系統崩潰這些問題。node

Node和操做系統有一種約定,若是建立了新的連接,操做系統就將通知Node,而後進入休眠。若是有人建立了新的連接,那麼它(Node)執行一個回調,每個連接只佔用了很是小的(內存)堆棧開銷。git

#1 Node.js的事件機制# Node.js在其Github代碼倉庫(https://github.com/joyent/node)上有着一句短短 的介紹:Evented I/O for V8 JavaScript。這句近似廣告語的句子卻道盡了 Node.js自身的特點所在:基於V8引擎實現的事件驅動IO。在本文的這部份內容中,來揭開這Evented這個關鍵詞的一切奧祕吧。程序員

Node.js可以在衆多的後端JavaScript技術之中脫穎而出,正是因其基於事件的特色而受到歡迎。拿Rhino來作比較,能夠看出Rhino引擎支持的後端JavaScript擺脫不掉其餘語言同步執行的影響,致使JavaScript在後端編程與前端編程之間有着十分顯著的差異,在編程模型上沒法造成統一。在前端編程中,事件的應用十分普遍,DOM上的各類事件在Ajax大規模應用以後,異步請求更獲得普遍的認同,而Ajax亦是基於事件機制的。在Rhino中,文件讀取等操做,均是同步操做進行的。在這類單線程的編程模型下,若是採用同步機 制,沒法與PHP之類的服務端腳本語言的成熟度媲美,性能也沒有值得可圈可點的部分。直到Ryan Dahl在2009年推出Node.js後,後端JavaScript才走出其迷局。Node.js的推出,該變了兩個情況:github

  1. 統一了先後端JavaScript的編程模型。
  2. 利用事件機制充分利用異步IO突破單線程編程模型的性能瓶頸,使得JavaScript在後端達到實用價值。

#2 事件機制的實現# Node.js中大部分的模塊,都繼承自Event模塊(http://nodejs.org/docs/latest/api/events.html)。Event模塊 (events.EventEmitter)是一個簡單的事件監聽器模式的實現。具備 addListener/on,once,removeListener,removeAllListeners,emit等基本的事件監聽模式的方法實現。它與前端DOM樹上的事件並不相同,由於它不存在冒泡,逐層捕獲等屬於DOM的事件行爲,也沒有preventDefault()、stopPropagation()、stopImmediatePropagation()等處理事件傳遞的方法。web

從另外一個角度來看,事件偵聽器模式也是一種事件鉤子(hook)的機制,利用事件鉤子導出內部數據或狀態給外部調用者。Node.js中的不少對象,大多具備黑盒的特色,功能點較少,若是不經過事件鉤子的形式,對象運行期間的中間值或內部狀態,是咱們沒法獲取到的。這種經過事件鉤子的方式,可使編程者不用關注組件是如何啓動和執行的,只需關注在須要的事件點上便可。數據庫

var options = {
    host: 'www.google.com', 
    port: 80,
    path: '/upload', 
    method: 'POST'
};

var req = http.request(options, function (res) {
    console.log('STATUS: ' + res.statusCode);
    console.log('HEADERS: ' + JSON.stringify(res.headers));
    res.setEncoding('utf8');
    res.on('data', function (chunk) {
        console.log('BODY: ' + chunk);
    });
});

req.on('error', function (e) {
    console.log('problem with request: ' + e.message);
});

// write data to request body 
req.write('data\n'); 
req.write('data\n'); 
req.end();

在這段HTTP request的代碼中,程序員只須要將視線放在error,data這些業務事件點便可,至於內部的流程如何,無需過於關注。編程

值得一提的是若是對一個事件添加了超過10個偵聽器,將會獲得一條警告,這一處設計與Node.js自身單線程運行有關,設計者認爲偵聽器太多,可能致使內存泄漏,因此存在這樣一個警告。調用:後端

emitter.setMaxListeners(0);

能夠將這個限制去掉。

其次,爲了提高Node.js的程序的健壯性,EventEmitter對象對error事件進行了特殊對待。若是運行期間的錯誤觸發了error事件,EventEmitter會檢查是否有對error事件添加過偵聽器,若是添加了,這個錯誤將會交由該偵聽器處理,不然,這個錯誤將會做爲異常拋出。若是外部沒有捕獲這個異常,將會引發線程的退出

#3 事件機制的進階應用# ##3.1 繼承event.EventEmitter## 實現一個繼承了EventEmitter類是十分簡單的,如下是Node.js中流對象繼承 EventEmitter的例子:

function Stream() { 
    events.EventEmitter.call(this);
}
util.inherits(Stream, events.EventEmitter);

Node.js在工具模塊中封裝了繼承的方法,因此此處能夠很便利地調用。程序員能夠經過這樣的方式輕鬆繼承EventEmitter對象,利用事件機制,能夠幫助你解決一些問題。

##3.2 多事件之間協做## 在略微大一點的應用中,數據與Web服務器之間的分離是必然的,如新浪微博、Facebook、Twitter等。這樣的優點在於數據源統一,而且能夠爲相同數據源制定各類豐富的客戶端程序。以Web應用爲例,在渲染一張頁面的時候,一般須要從多個數據源拉取數據,並最終渲染至客戶端。Node.js在這種場景中能夠很天然很方便的同時並行發起對多個數據源的請求

api.getUser("username", function (profile) {
    // Got the profile
});
api.getTimeline("username", function (timeline) {
    // Got the timeline
});
api.getSkin("username", function (skin) {
    // Got the skin
});

Node.js經過異步機制使請求之間無阻塞,達到並行請求的目的,有效的調用下層資源。可是,這個場景中的問題是對於多個事件響應結果的協調並不是被Node.js原生優雅地支持。爲了達到三個請求都獲得結果後才進行下一個步驟, 程序也許會被變成如下狀況:

api.getUser("username", function (profile) { 
    api.getTimeline("username", function (timeline) {
        api.getSkin("username", function (skin) { 
            // TODO
        }); 
    });
});

這將致使請求變爲串行進行,沒法最大化利用底層的API服務器。

爲解決這類問題,我曾寫做一個模塊(EventProxy,https://github.com/JacksonTian/eventproxy)來實現多事件協做,如下爲上面代碼的改進版:

var proxy = new EventProxy(); 
proxy.all("profile", "timeline", "skin", function (profile, timeline, skin) {
    // TODO
});
api.getUser("username", function (profile) {
    proxy.emit("profile", profile);    
});
api.getTimeline("username", function (timeline) { 
    proxy.emit("timeline", timeline);
});
api.getSkin("username", function (skin) {
    proxy.emit("skin", skin);
});

EventProxy也是一個簡單的事件偵聽者模式的實現,因爲底層實現跟Node.js的EventEmitter不一樣,沒法合併進Node.js中。可是卻提供了比EventEmitter更強大的功能,且API保持與EventEmitter一致,與Node.js的思路保持契合,並能夠適用在前端中。

這裏的all方法是指偵聽完profile、timeline、skin三個方法後,執行回調函數,並將偵聽接收到的數據傳入

最後還介紹一種解決多事件協做的方案:

Jscex(https://github.com/JeffreyZhao/jscex)。Jscex經過運行時編譯的思路 (須要時也可在運行前編譯),將同步思惟的代碼轉換爲最終異步的代碼來執行,能夠在編寫代碼的時候經過同步思惟來寫,能夠享受到同步思惟的便利寫做,異步執行的高效性能。若是經過Jscex編寫,將會是如下形式:

var data = $await(Task.whenAll({
    profile: api.getUser("username"), 
    timeline: api.getTimeline("username"),
    skin: api.getSkin("username")        
}));
// 使用data.profile, data.timeline, data.skin 
// TODO

##3.3 利用事件隊列解決雪崩問題## 所謂雪崩問題,是在緩存失效的情景下,大併發高訪問量同時涌入數據庫中查詢,數據庫沒法同時承受如此大的查詢請求,進而往前影響到網站總體響應緩慢。那麼在Node.js中如何應付這種情景呢。

var select = function (callback) { 
    db.select("SQL", function (results) {
        callback(results);    
    });
};

以上是一句數據庫查詢的調用,若是站點恰好啓動,這時候緩存中是不存在數 據的,而若是訪問量巨大,同一句SQL會被髮送到數據庫中反覆查詢,影響到 服務的總體性能。一個改進是添加一個狀態鎖

var status = "ready";
var select = function (callback) {
    if (status === "ready") {
        status = "pending";

        db.select("SQL", function (results) {
            callback(results);
            status = "ready";
        });
    } 
};

可是這種情景,連續的屢次調用select發,只有第一次調用是生效的,後續的select是沒有數據服務的。因此這個時候引入事件隊列吧:

var proxy = new EventProxy();
var status = "ready";
var select = function (callback) {
    proxy.once("selected", callback); 
    if (status === "ready") {
        status = "pending";
        db.select("SQL", function (results) {
            proxy.emit("selected", results);
            status = "ready";        
        });
    }
};

這裏利用了EventProxy對象的once方法,將全部請求的回調都壓入事件隊列中,並利用其執行一次就會將監視器移除的特色,保證每個回調只會被執行一次。對於相同的SQL語句,保證在同一個查詢開始到結束的時間中永遠只有一次,在這查詢期間到來的調用,只需在隊列中等待數據就緒便可,節省了重複的數據庫調用開銷。因爲Node.js單線程執行的緣由,此處無需擔憂狀態問題。這種方式其實也能夠應用到其餘遠程調用的場景中,即便外部沒有緩存策略,也能有效節省重複開銷。此處也能夠用EventEmitter替代EventProxy,不過 可能存在偵聽器過多,引起警告,須要調用setMaxListeners(0)移除掉警告,或者設更大的警告閥值

#4 簡單事件示例## 由於Node採用的是事件驅動的模式,其中的不少模塊都會產生各類不一樣的事件,可由模塊來添加事件處理方法,全部可以產生事件的對象都是事件模塊中的EventEmitter類的實例。代碼是全世界通用的語言,因此咱們仍是用代碼說話:

// 使用require()方法添加了events模塊並把返回值賦給了一個變量
var events = require("events");

// 建立了一個事件觸發器,也就是所謂的事件模塊中的 EventEmitter 類的實例
var emitter = new events.EventEmitter(); 

// on(event, listener)用來爲某個事件 event 添加事件處理方法監聽器
emitter.on("myEvent", function(msg) { 
    console.log(msg); 
}); 

// emit(event, [arg1], [arg2], [...]) 方法用來產生事件。以提供的參數做爲監聽器函數的參數,順序執行監聽器列表中的每一個監聽器函數。
emitter.emit("myEvent", "Hello World.");

EventEmitter 類中的方法都與事件的產生和處理相關:

  1. addListener(event, listener) 和 on(event, listener) 這兩個方法都是將一個監聽器添加到指定事件的監聽器數組的末尾

  2. once(event, listener) 這個方法爲事件爲添加一次性的監聽器。該監聽器在事件第一次觸發時執行,事後將被移除

  3. removeListener(event, listener) 該方法用來將監聽器從指定事件的監聽器數組中移除出去

  4. emit(event, [arg1], [arg2], [...]) 剛剛提到過了。

在Node中,存在各式各樣不一樣的數據流,Stream(流)是一個由不一樣對象實現的抽象接口。例如請求HTTP服務器的request是一個流,相似於stdout(標準輸出);包括文件系統、HTTP 請求和響應、以及 TCP/UDP 鏈接等。流能夠是可讀的,可寫的,或者既可讀又可寫。全部流都是EventEmitter的實例,所以能夠產生各類不一樣的事件。可讀流主要會產生如下事件:

data 當讀取到流中的數據時,此事件被觸發

end 當流中沒有數據可讀時,此事件被觸發

error 當讀取數據出現錯誤時,此事件被觸發

close 當流被關閉時,此事件被觸發,但是並非全部流都會觸發這個事件。(例如,一個鏈接進入的HTTP request流就不會觸發'close'事件。)

還有一種比較特殊的 fd 事件,當在流中接收到一個文件描述符時觸發此事件。只有UNIX流支持這個功能,其餘類型的流均不會觸發此事件。

相關文章
相關標籤/搜索