認識node核心模組--深入EventEmitter

莫凡_Tcg發表於2017-11-03

原文地址在我的部落格,轉載請註明出處,謝謝!

node 採用了事件驅動機制,而EventEmitter 就是node實現事件驅動的基礎。在EventEmitter的基礎上,node 幾乎所有的模組都繼承了這個類,以實現非同步事件驅動架構。繼承了EventEmitter的模組,擁有了自己的事件,可以繫結/觸發監聽器,實現了非同步操作。EventEmitter是node事件模型的根基,由EventEmitter為基礎構建的事件驅動架構處處體現著非同步程式設計的思想,因此,我們在構建node程式時也要遵循這種思想。EventEmitter實現的原理是觀察者模式,這也是實現事件驅動的基本模式。本文將圍繞EventEmitter,從中探討它的原理觀察者模式、體現的非同步程式設計思想以及應用。

正文

events模組的EventEmitter類

node 的events模組只提供了一個EventEmitter類,這個類實現了node非同步事件驅動架構的基本模式——觀察者模式,提供了繫結事件和觸發事件等事件監聽器模式一般都會提供的API:

const EventEmitter = require('events')

class MyEmitter extends EventEmitter {}
const myEmitter = new MyEmitter()

function callback() {
    console.log('觸發了event事件!')
}
myEmitter.on('event', callback)
myEmitter.emit('event')
myEmitter.removeListener('event', callback);複製程式碼

只要繼承EventEmitter類就可以擁有事件、觸發事件等,所有能觸發事件的物件都是 EventEmitter 類的例項。

而觀察者模式(事件釋出/訂閱模式)就是實現EventEmitter類的基本原理,也是事件驅動機制基本模式。

事件驅動原理:觀察者模式

在事件驅動系統裡,事件是如何產生的?一個事件發生為什麼能”自動”呼叫回撥函式?我們先看看觀察者模式。

觀察者(Observer)模式是一種設計模式,應用場景是當一個物件的變化需要通知其他多個物件而且這些物件之間需要鬆散耦合時。在這種模式中,被觀察者(主體)維護著一組其他物件派來(註冊)的觀察者,有新的物件對主體感興趣就註冊觀察者,不感興趣就取消訂閱,主體有更新的話就依次通知觀察者們。說猿話就是:

function Subject() {
    this.listeners = {}
}

Subject.prototype = {
    // 增加事件監聽器
    addListener: function(eventName, callback) {
        if(typeof callback !== 'function')
            throw new TypeError('"listener" argument must be a function')

        if(typeof this.listeners[eventName] === 'undefined') {
            this.listeners[eventName] = []
        } 
        this.listeners[eventName].push(callback) // 放到觀察者物件中
    },
    // 取消監聽某個回撥
    removeListener: function(eventName, callback) {
        if(typeof callback !== 'function')
            throw new TypeError('"listener" argument must be a function')
        if(Array.isArray(this.listeners[eventName]) && this.listeners[eventName].length !== 0) {
            var callbackList = this.listeners[eventName]
            for (var i = 0, len=callbackList.length; i < len; i++) {
                if(callbackList[i] === callback) {
                    this.listeners[eventName].splice(i,1)     // 找到監聽器並從觀察者物件中刪除
                }
            }

        }
    },
    // 觸發事件:在觀察者物件裡找到這個事件對應的回撥函式佇列,依次執行
    triggerEvent: function(eventName,...args) {
        if(this.listeners[eventName]) {
            for(var i=0, len=this.listeners[eventName].length; i<len; i++){
                this.listeners[eventName][i](...args)
            }
        }
    }
}複製程式碼

OK,我們現在來新增監聽器和傳送事件:

var event = new Subject()
function hello() {
    console.log('hello, there')
}
event.addListener('hello', hello)
event.triggerEvent('hello')     //    輸出 hello, there
event.removeListener('hello', hello) // 取消監聽
setTimeout(() => event.triggerEvent('hello'),1000) // 過了一秒什麼也沒輸出複製程式碼

在觀察者模式中,註冊的回撥函式即事件監聽器,觸發事件呼叫各個回撥函式即是釋出訊息。

你可以看到,觀察者模式只不過維護一個訊號對應函式的列表,可以存,可以除,你只要給它訊號(索引),它就按照這個訊號執行對應的函式,也就相當於間接呼叫了。那直接呼叫函式不就行了,幹嘛寫的那麼拐彎抹角?剛才也說了,這是因為觀察者模式能夠解耦物件之間的關係,實現了表示層和資料邏輯層的分離,並定義了穩定的更新訊息傳遞機制。

回到開始的問題,事件是如何產生又“自動”被呼叫的?是像上面那樣當呼叫event.triggerEvent的時侯產生的嗎?並不是,呼叫event.triggerEvent就相當於呼叫了回撥函式,是事件執行過程,而事件產生過程則更多由底層來產生並通知給node的。我們拿node的全域性變數 process來舉例,process是EventEmitter的例項:

process.on('exit', (code) => {
  console.log(`About to exit with code: ${code}`);
});複製程式碼

node執行時會在process的exit事件上繫結你指定的回撥,相當於呼叫了上面的addListener,而當你退出程式時,你會發現你指定的函式被執行了,但是你沒有手動呼叫觸發exit事件的方法,也就是上面的triggerEvent,這是因為node底層幫你呼叫了——作業系統底層使這個程式退出了,node會得到這個資訊,然後觸發事先定義好的觸發方法,回撥函式就因此依次執行了。像這樣的內建事件是node模組事先寫好並開放出來的,使用時直接繫結回撥函式即可,如果要自定義事件,那就得自己傳送訊號了。

上面程式碼實現了最基本的觀察者模式,node 原始碼中EventEmitter的實現原理跟這差不多,除了這些還加入了其他有用的特性,而且各種實現都儘可能使用效能最好的方式(node原始碼真是處處反映著智慧的光芒)。

node中眾多模組都繼承了EventEmitter,比如檔案模組系統下的FSWatcher

const EventEmitter = require('events')
const util = require('util')
...

function FSWatcher() {
  EventEmitter.call(this);// 呼叫建構函式
  ...
}
util.inherits(FSWatcher, EventEmitter); // 繼承 EventEmitter複製程式碼

其他模組也是如此。它們一同組成了node的非同步事件驅動架構。

非同步程式設計正規化

可以看到,由於採用事件模型和非同步I/O,node中大量模組的API採用了非同步回撥函式的方式,底層也處處體現了非同步程式設計的方式。雖然非同步也帶來了很多問題——理解困難、回撥巢狀過深、錯誤難以捕捉、多執行緒程式設計困難等,不過相比於非同步帶來的高效能,加上這些問題都有比較好的解決方案,非同步程式設計正規化還是很值得嘗試的,尤其對於利用node構建應用程式的時候。

從最基本的回撥函式開始

回撥函式是非同步程式設計的體現,而回撥函式的實現離不開高階函式。得益於javascript語言的靈活性,函式作為引數或返回值,而將函式作為引數或返回值的函式就是高階函式:

function foo(x,bar) {
    return bar(x)
}// 對於相同的foo,傳進去不同的bar就有不同的操作結果

var arr = [2,3,4,5]
arr.forEach(function(item,index){
    // do something for every item
}) // 陣列的高階函式

event.addListener('hello', hello) // 還有上面觀察者模式實現的addListener複製程式碼

基於高階函式的特性,就可以實現回撥函式的模式。實際上,正式因為javascript函式用法非常靈活,才有高階函式和眾多設計模式。

採用事件釋出/訂閱模式(觀察者模式)

單純地使用高階函式特性不足以構建簡單、靈活、強大的非同步程式設計模式的應用程式,我們需要從其他語言借鑑一些設計模式。就像上面提到的,node的events模組實現了事件釋出/訂閱模式,這是一種廣泛用於非同步程式設計的模式。它將回撥函式事件化,將事件與各回撥函式相關聯,註冊回撥函式就是新增事件監聽器,這些事件監聽器可以很方便的新增、刪除、被執行,使得事件和處理邏輯(註冊的回撥函式)之間輕鬆實現關聯和解耦——事件釋出者無需關注監聽器是如何實現業務邏輯的,也不用關注有多少個事件監聽器,只需按照訊息執行即可,而且資料通過這種訊息的方式可以靈活的傳遞。

不僅如此,這種模式還可以實現像類一樣的對功能進行封裝:將不變的邏輯封裝在內部,將需要自定義、容易變化的部分通過事件暴露給外部定義。Node中很多物件大多都有這樣黑盒子的特點,通過事件鉤子,可以使使用者不用關注這個物件是如何啟動的,只需關注自己關注的事件即可。

像大多數node核心模組一樣,繼承EventEmitter,我們就可以使用這種模式,幫助我們以非同步程式設計方式構建node程式。

利用Promise

Promise是CommonJs釋出的一個規範,它的出現給非同步程式設計帶來了方便。Promise所作的只是封裝了非同步呼叫、巢狀回撥,使得原本複雜巢狀邏輯不清的回撥變得優雅和容易理解。有了Promise的封裝,你可以這樣寫非同步呼叫:

function fn1(resolve, reject) {
    setTimeout(function() {
        console.log('步驟一:執行');
        resolve('1');
    },500);
}

function fn2(resolve, reject) {
    setTimeout(function() {
        console.log('步驟二:執行');
        resolve('2');
    },100);
}

new Promise(fn1).then(function(val){
    console.log(val);
    return new Promise(fn2);
}).then(function(val){
    console.log(val);
    return 33;
}).then(function(val){
    console.log(val);
});複製程式碼

那Promise是如何封裝的呢?

首先,Promise經常用於處理非同步、延時操作,為了放在then裡面的”接下來要做的事“以正確的順序被執行,Promise被設計為狀態機,狀態變化為pending => resolve(成功)、pending => reject(失敗),而且,Promise還維護成功或失敗時要執行的函式List,List中的回撥正是Promise處在pending狀態時將then中註冊的回撥push進去的;Promise內部有一個resolve和reject函式,分別在成功/失敗時執行函式List,並且這兩個函式會傳遞給回撥函式,由使用者決定什麼時候resolve/reject;為了實現鏈式呼叫,then中返回的是promise:

function getUserId() {
    return new Promise(function(resolve, i) {
        //非同步請求
        setTimeout(function(){
            console.log('非同步操作成功,下一步執行promise的'+i+'的resolve')
            resolve('Fuck you Promise!', i)
        },1000)
    }, 1)
}

getUserId().then(function(words) {
    console.log(words)
})

// 實現
function Promise(fn, i) {
    var i = i
    var state = 'pending'
    var result = null
    var promises = []
    console.log('Promise' + i + 'constructing')

    this.then = function(onFulfilled) {
        console.log('then被呼叫')
        return new Promise(function(resolve) {
            console.log('返回一個promise')
            handle({
                onFulfilled: onFulfilled || null,
                resolve: function(ret, i) {resolve(ret,i)}
            })
        },2)
    }

    function handle(promise) {
        if(state === 'pending') {
            console.log('promise' + i + '還在pending中')
            promises.push(promise)
            console.log('註冊回撥')
            return
        }

        if(!promise.onFulfilled) {
            console.log('回撥為空,resolve結果')
            promise.resolve(result, i)
            return
        }
        console.log('執行回撥')
        var ret = promise.onFulfilled(result)
        console.log('處理回撥返回的值(可能是另一個promise)')
        promise.resolve(ret, 2)

    }

    function resolve (newResult, i) {
        console.log('執行promise' + i + '的resolve')
        if(newResult && (typeof newResult === 'object' || typeof newResult === 'function')) {
            console.log('then中註冊的回撥返回了promise')
            var then = newResult.then
            if(typeof then === 'function') {
                console.log('呼叫then')
                then.call(newResult, resolve)
            }
        }
        console.log('設定promise' + i + '的狀態為fulfilled')
        state = 'fulfilled'
        result = newResult
        setTimeout(function(){
            console.log('遍歷promise' + i + '註冊的回撥執行')
            console.log(promises[0])
            promises.forEach(function(promise) {
                handle(promise)
            });
        },0)

    }
    console.log('傳resolve到promise' + i + '函式引數')
    fn(resolve, i)
}複製程式碼

注意,這是Promise/A+規範的簡單實現,還有reject原理一樣的。我在這裡為了更好的理解promise,不至於弄混亂,加入了標號,方便理解,Promise/A+規範裡並沒有。

實際上,node高版本已經支援promise了,可以直接使用,但不如Bluebird這類三方庫快,而且Bluebird擴充套件了很多Promise/A+沒有的方法。

使用第三方庫Async/Step

async是著名的流程控制庫,經常被npm install,它提供了20多個方法幫助我們處理非同步協作模式。比如:

  • series ——非同步任務的序列執行,就像Promise一樣,只不過形式不同
  • parallel——非同步任務並行執行,相當於Promise.all
  • waterfall——處理具有依賴關係的非同步呼叫,比如前一個結果是後一個輸入
  • auto——自動分析非同步呼叫的依賴關係,引數是一個依賴關係物件
  • ...

Step比async更輕量、更簡單,只有一個介面Step, 在介面裡可以呼叫Step提供的方法,功能與async差不多。

非同步程式設計正規化遠不止這麼多,還有很多重要的思想、設計模式,還有一些需要在實踐中去發現、總結。

總結

EventEmitter提供的介面非常簡單,但是它背後體現的思想貫穿了Node整個架構。Node不是第一個使用非同步程式設計的平臺,但非同步架構在Node中處處體現,是Node設計的基本思想。在學習node時,透過現象看本質、深入淺出,是一個明智的方法,對待任何事物也是如此。

參考文獻:

相關文章