本次嘗試淺析Node.js中的EventEmitter模塊的事件機制,分析在Node.js中實現發佈訂閱模式的一些細節。完整Node.js源碼點這裏。html
歡迎關注個人博客,不按期更新中——node
大多數 Node.js 核心 API 都採用慣用的異步事件驅動架構,其中某些類型的對象(觸發器)會週期性地觸發命名事件來調用函數對象(監聽器)。例如,net.Server 對象會在每次有新鏈接時觸發事件;fs.ReadStream 會在文件被打開時觸發事件;流對象 會在數據可讀時觸發事件。全部能觸發事件的對象都是 EventEmitter 類的實例。
Node.js中對EventEmitter類的實例的運用能夠說是貫穿整個Node.js,相信這一點你們已是很熟悉的了。其中所運用到的發佈訂閱模式,則是很經典的管理消息分發的一種方式。在這種模式中,發佈消息的一方不須要知道這個消息會給誰,而訂閱的一方也無需知道消息的來源。使用方式通常以下:git
const EventEmitter = require('events'); class MyEmitter extends EventEmitter {} const myEmitter = new MyEmitter(); myEmitter.on('event', () => { console.log('觸發了一個事件A!'); }); myEmitter.emit('event'); //觸發了一個事件A!
當咱們訂閱了'event'事件後,能夠在任何地方經過emit('event')
來執行事件回調,EventEmitter至關於一箇中介,負責記錄都訂閱了哪些事件而且觸發後的回調是什麼,當事件被觸發,就將回調一一執行。github
從源碼中看下EventEmitter類的是如何實現發佈訂閱的。
首先咱們梳理一下實現這個模式須要的步驟:chrome
在生成空對象的方式中,通常容易想到的是直接進行賦值空對象即 var a = {};
,Node.js中採用的方式爲var a = Object.create(null)
,使用這種方式理論上是應該對對象的屬性存取的操做更快,出於好奇做者對這兩種方式作了個粗略的對比:api
var a = {} a.test = 1 var b = Object.create(null) b.test = 1 console.time('{}') for(var i = 0; i < 1000; i++) { console.log(a.test) } console.timeEnd('{}') console.time('create') for(var i = 0; i < 1000; i++) { console.log(b.test) } console.timeEnd('create')
打印結果顯示出來貌似直接用空對象賦值與經過Object.create的方式並無很大的性能差別,而且尚未誰必定佔了上風,就目前該空對象用來存儲註冊的監聽事件與回調來看,若是直接用{}來初始化this._events性能方面影響也許不大。不過這一點只是我的觀點,暫時還並不能領會Node裏面如此運用的深意。數組
EventEmitter.prototype.addListener = function addListener(type, listener) { return _addListener(this, type, listener, false); }; EventEmitter.prototype.on = EventEmitter.prototype.addListener;
添加監聽者的方法爲addListener,同時on是其別名。瀏覽器
if (!existing) { // Optimize the case of one listener. Don't need the extra array object. existing = events[type] = listener; ++target._eventsCount; } else { if (typeof existing === 'function') { // Adding the second element, need to change to array. existing = events[type] = prepend ? [listener, existing] : [existing, listener]; } else { // If we've already got an array, just append. if (prepend) { existing.unshift(listener); } else { existing.push(listener); } } ... }
若是以前不存在監聽事件,則會進入第一個判斷內,其中type爲事件類型,listener爲觸發的事件回調。若是以前註冊過事件,那麼回調函數會添加到回調隊列的頭或尾。看以下打印結果:架構
myEmitter.on('event', () => { console.log('觸發了一個事件A!'); }); myEmitter.on('event', () => { console.log('觸發了一個事件B!'); }); myEmitter.on('talk', () => { console.log('觸發了一個事件CS!'); // myEmitter.emit('talk'); }); console.log(myEmitter._events) //{ event: [ [Function], [Function] ], talk: [Function] }
myEmitter實例的_events方法就是咱們存儲事件與回調的對象,能夠看到當咱們依次註冊事件後,回調會被推到 _events對應key的value中。app
在觸發的emit函數中,會根據觸發時傳入參數的多少執行不一樣的函數:(參數不一樣直接執行不一樣的函數,這個操做應該會讓性能更好,不過做者沒有測試這點)
switch (len) { // fast cases case 1: emitNone(handler, isFn, this); break; case 2: emitOne(handler, isFn, this, arguments[1]); break; case 3: emitTwo(handler, isFn, this, arguments[1], arguments[2]); break; case 4: emitThree(handler, isFn, this, arguments[1], arguments[2], arguments[3]); break; // slower default: args = new Array(len - 1); for (i = 1; i < len; i++) args[i - 1] = arguments[i]; emitMany(handler, isFn, this, args); }
以emitMany爲例看下內部觸發實現:
var isFn = typeof handler === 'function'; function emitMany(handler, isFn, self, args) { if (isFn) //handler類型爲函數,即對這個事件只註冊了一個監聽函數 handler.apply(self, args); else { //當對同一事件註冊了多個監聽函數的時候,handler類型爲數組 var len = handler.length; var listeners = arrayClone(handler, len); for (var i = 0; i < len; ++i) listeners[i].apply(self, args); } } function arrayClone(arr, n) { var copy = new Array(n); for (var i = 0; i < n; ++i) copy[i] = arr[i]; return copy; }
源碼中實現了arrayClone
方法,來複制一份一樣的監聽函數,再去依次執行副本。我的對這個作法的理解是,當觸發當前類型事件後,就鎖定須要執行的回調函數隊列,不然當觸發回調過程當中,再去推入新的回調函數,或者刪除已有回調函數,容易形成不可預知的問題。
若是回調事件只有一個那麼直接刪除便可,若是是數組就像以前看到的那樣註冊了多組對一樣事件的監聽,就要涉及從數組中刪除項的實現。在這裏Node本身實現了一個spliceOne函數來代替原生的splice,而且說明其方式比splice快1.5倍。下面是做者進行的簡易粗略,不嚴謹的運行時間比較:
上面作了一個很粗略的運算時間比較,一樣是對長度爲1000的數組第100項進行刪除操做,而且代碼運行在chrome瀏覽器下(版本號61.0.3163.100)node源碼中本身實現的方法確實比原生的splice快了一些,不過結果只是一個參考畢竟這個對比很粗略,有興趣的童鞋能夠寫一組benchmark來進行對比。
源碼的邊界狀況比較多。在這裏只作一個相對簡單的流程淺析,哪裏說明有誤歡迎指正~
PS:相關實例源碼:https://github.com/Aaaaaaaty/...
慣例po做者的博客,不定時更新中——有問題歡迎在issues下交流。