認識node核心模塊--深入EventEmitter
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, thereevent.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時,透過現象看本質、深入淺出,是一個明智的方法,對待任何事物也是如此。
參考文獻:
- https://segmentfault.com/a/1190000009478377
- 【朴靈】《深入淺出Node.Js》
推薦閱讀:
※深入理解MySQL――鎖、事務與並發控制 這才是正確的!
※去哪兒 Api 自動化測試實踐
※伺服器端編程心得(八)——高性能伺服器架構設計總結——以flamigo伺服器代碼為例
※[譯] 將一個舊的大型項目遷移到 Python 3