深刻淺出Node.js(四):Node.js的事件機制

Node.js的事件機制

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

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

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

有了第二次瀏覽器大戰中的佼佼者V8的適時助力,使得Node.js在短短的兩年內達到可觀的運行效率,並迅速被你們接受。這一點從Node.js項目在Github上的流行度和NPM上的庫的數量可見一斑。 node

至於Node.js爲什麼會選擇Evented I/O for V8 JavaScript的結構和形式來實現,能夠參見一下2011年初對做者Ryan Dahl的一次採訪:http://bostinno.com/2011/01/31/node-js-interview-4-questions-with-creator-ryan-dahl/ 。 git

事件機制的實現

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() 等處理事件傳遞的方法。 程序員

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

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事件添加過偵聽器,若是添加了,這個錯誤將會交由該偵聽器處理,不然,這個錯誤將會做爲異常拋出。若是外部沒有捕獲這個異常,將會引發線程的退出。 api

事件機制的進階應用

繼承event.EventEmitter

實現一個繼承了EventEmitter類是十分簡單的,如下是Node.js中流對象繼承EventEmitter的例子:

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

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

多事件之間協做

在略微大一點的應用中,數據與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

此節感謝Jscex做者@老趙(http://blog.zhaojie.me/)的指正和幫助。

利用事件隊列解決雪崩問題

所謂雪崩問題,是在緩存失效的情景下,大併發高訪問量同時涌入數據庫中查詢,數據庫沒法同時承受如此大的查詢請求,進而往前影響到網站總體響應緩慢。那麼在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)移除掉警告,或者設更大的警告閥值。

相關文章
相關標籤/搜索