理解Nodejs的Event Loop

Node的「event loop」主要是用來處理高輸出量的。這很神奇,這也是爲何node能夠在單線程的狀況下同時處理不少的後臺操做。本文就會集中講述event loop是怎麼運行的,這樣你能夠可使用這個神奇的東西完成你本身的工做。javascript

 

事件驅動的編程(event-driven programming)java

要理解event loop首先須要瞭解的就是event driven programming(事件驅動的編程)。這個在1960年代就已經被人們所熟知。現在,event-driven proggramming被普遍的應用在UI處理中。javascript主要用在處理DOM中。node

定義很是簡單:event-driven programming就是程序的控制流程是由事件或者狀態的改變決定的。主要的實現機制就是用一箇中心控制檯監聽事件,並在事件發生的時候調用這個事件對應的回調函數(狀態的改變也是同樣)。很熟悉吧?這就是node的event loop的處理機制。編程

瀏覽器中的javascript開發中經常會遇到.on*()的方法,好比element.onclick(),用於鏈接用戶操做和DOM。這個模式在一個單一元素能夠發出多種可能的事件的時候工做的很是好。Node在EventEmitter中使用了這個模式,這個模式主要用在了server,socket和‘http’等模塊中。這在一個實例須要發出多種類型的事件和狀態的時候很是有用。瀏覽器

另外一個廣泛使用的模式是成功和失敗。主流的實現方法有兩種。第一個是發生錯誤的回調(原文:「error back」),就是在發生錯誤的時候把error做爲第一個參數調用回調函數。另外一種方法是在ES6中定義的,使用Promises。閉包

‘fs’模塊中大量使用了‘error back’。技術上來講,某些調用會發出其餘的事件。好比,fs.readFile()。可是隻在成功或者失敗的時候才提醒用戶。API這麼設計主要是出於系統策略,不是技術的限制。app

兩一個很大的誤解是事件發生機制自己是異步的。可是,這是不對的。下面的代碼會代表這一點:異步

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

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

MyEmitter.prototype.doStuff = function() {
    console.log('before');
    this.emit('fire');
    console.log('after');
}

var me = new MyEmitter();
me.on('fire', function(){
    console.log('emit fired');
});

me.doStuff();

// Output:
// before
// emit fired
// after

EventEmitter看起來是異步的,由於他老是被用來發出異步操做完成的信號。可是,EventEmitter API是徹底同步的。emit方法可能被異步調用,可是須要主要到所有的監聽方法都是按照添加的順序同步執行的。socket

 

總覽async

Node自己依賴於不少的庫。其中之一就是libuv。這個庫就是用來處理隊列和異步的事件的。Node極大限度的使用了操做系統核心已經有的功能。申請寫操做,保持鏈接以及更多地由系統處理的功能。好比,鏈接申請被系統排隊,直到被Node處理。

你也許瞭解過Node有一個線程池,也會想知道「若是node把這些職責都推掉了,那還須要什麼線程池?」 這是由於系統核心並不支持什麼事都異步執行。好比,有時node須要鎖定某個線程,這樣event loop能夠一直執行而不至於死鎖。

這裏有一個簡化的圖來解釋event loop是怎麼運行的。

diagram

有一些event loop的內部執行機制很難在圖中給出:

  • 全部使用process.nexTick()指定的回調都會在event loop的某階段的最後時刻,在進入下一個階段前被執行(好比,timer)。這樣有一個潛在的風險,若是process.nextTrick()方法有遞歸調用的話,整個event loop就被拖死了。
  • 「待處理隊列(pending callbacks)」就是未被其餘階段(phase)處理的回調隊列(好比:給fs.write()傳進去的回調)。

 

Event Emitter和Event Loop

爲了簡化和Event loop的互操做,因此有了EventEmitter。用EventEmitter能夠很容易建立一個基於事件的API。下面咱們就一些主要內容作講解。

下面的代碼展現了沒有同步發出事件會形成用戶錯過事件的狀況:

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

function MyThing() {
    EventEmitter.call(this);
    
    doFirstThing();
    this.emit('thing1');
}
util.inherites(MyThing, EventEmitter);

var mt = new MyThing();

mt.on('thing1', function(){
    // never going to happen.
});

以上代碼的問題就在於‘thing1’永遠不會被用戶捕捉到,由於MyThing()必須在初始化完成以後才能監聽事件。下面是一個簡單地解決方案,不須要任何另外的閉包:

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

function MyThing() {
    EventEmitter.call(this);
    
    // doFirstThing();
    // this.emit('thing1');
    setImmediate(emitThing1, this);
}
util.inherits(MyThing, EventEmitter);

function emitThing1(self) {
    self.emit('thing1');
}

var mt = new MyThing();

mt.on('thing1', function(){
    // bravo
    console.log('bravo, thing1 captured.');
});

// bravo, thing1 captured.

下面的代碼頁能夠運行,只不過消耗較多。

function MyThing() {
    EventEmitter.call(this);
    
    // doFirstThing();
    // this.emit('thing1');
    // setImmediate(emitThing1, this);
    setImmediate(this.emit.bind(this, 'thing1'));
}

另外一種狀況是觸發錯誤。查出你的應用的問題很是麻煩,並且若是沒有調用棧的話,簡直沒法排查。一個Error在異步執行的深處初始化的時候可能會致使調用棧丟失。解決這個問題最靠譜的兩個辦法就是同步emit事件,或者確保Error帶了足夠的相關信息。請看如下代碼的演示:

MyThing.prototype.foo = function () {
    var er = doFirstThing();
    if (er) {
        // emit error asynchronously
        setImmediate(emitError, this, new Error('Bad stuff'));
        return;
    }
    
    // emit error synchronously
    var er = doSecondThing();
    if (er) {
        this.emit('error', 'More bad stuff');
        return;
    }
};

emit的錯誤應該當即被處理,以防程序繼續執行。並且在構造函數中emit錯誤也不是個好主意。

 

最後

本文只介紹了event loop知識的一小部分。這些會在以後的文章中補足。可是,這是繼續下去以前必備的基礎。以後的文章會講述event loop是怎麼和系統的核心互相交互的。

相關文章
相關標籤/搜索