寫在最前
本次嘗試淺析Node.js中的EventEmitter模塊的事件機制,分析在Node.js中實現發佈訂閱模式的一些細節。完整Node.js源碼點這裏。
歡迎關注我的博客,不定期更新中——
EventEmitter
大多數 Node.js 核心 API 都採用慣用的異步事件驅動架構,其中某些類型的對象(觸發器)會週期性地觸發命名事件來調用函數對象(監聽器)。例如,net.Server 對象會在每次有新連接時觸發事件;fs.ReadStream 會在文件被打開時觸發事件;流對象 會在數據可讀時觸發事件。所有能觸發事件的對象都是 EventEmitter 類的實例。
Node.js中對EventEmitter類的實例的運用可以說是貫穿整個Node.js,相信這一點大家已經是很熟悉的了。其中所運用到的發佈訂閱模式,則是很經典的管理消息分發的一種方式。在這種模式中,發佈消息的一方不需要知道這個消息會給誰,而訂閱的一方也無需知道消息的來源。使用方式一般如下:
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相當於一箇中介,負責記錄都訂閱了哪些事件並且觸發後的回調是什麼,當事件被觸發,就將回調一一執行。
發佈訂閱模式
從源碼中看下EventEmitter類的是如何實現發佈訂閱的。
首先我們梳理一下實現這個模式需要的步驟:
1. 初始化空對象用來存儲監聽事件與對應的回調函數
2. 添加監聽事件,註冊回調函數
3. 觸發事件,找出對應回調函數隊列,一一執行
4. 刪除監聽事件
初始化空對象
在生成空對象的方式中,一般容易想到的是直接進行賦值空對象即var a = {};
,Node.js中採用的方式爲var a = Object.create(null)
,使用這種方式理論上是應該對對象的屬性存取的操作更快,出於好奇作者對這兩種方式做了個粗略的對比:
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中。
觸發事件,找出對應回調函數隊列,一一執行
在觸發的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/Blog/blob/master/node/event.js
慣例po作者的博客,不定時更新中——
有問題歡迎在issues下交流。