摘錄自:https://segmentfault.com/a/1190000012712840
寫在最前
本次嘗試淺析Node.js中的EventEmitter模塊的事件機(jī)制慕趴,分析在Node.js中實(shí)現(xiàn)發(fā)布訂閱模式的一些細(xì)節(jié)。
EventEmitter
大多數(shù) Node.js 核心 API 都采用慣用的異步事件驅(qū)動(dòng)架構(gòu),其中某些類型的對(duì)象(觸發(fā)器)會(huì)周期性地觸發(fā)命名事件來(lái)調(diào)用函數(shù)對(duì)象(監(jiān)聽器)岳悟。例如波闹,net.Server 對(duì)象會(huì)在每次有新連接時(shí)觸發(fā)事件;fs.ReadStream 會(huì)在文件被打開時(shí)觸發(fā)事件;流對(duì)象 會(huì)在數(shù)據(jù)可讀時(shí)觸發(fā)事件篙贸。所有能觸發(fā)事件的對(duì)象都是 EventEmitter 類的實(shí)例颊艳。
Node.js中對(duì)EventEmitter類的實(shí)例的運(yùn)用可以說(shuō)是貫穿整個(gè)Node.js茅特,相信這一點(diǎn)大家已經(jīng)是很熟悉的了。其中所運(yùn)用到的發(fā)布訂閱模式棋枕,則是很經(jīng)典的管理消息分發(fā)的一種方式白修。在這種模式中,發(fā)布消息的一方不需要知道這個(gè)消息會(huì)給誰(shuí)重斑,而訂閱的一方也無(wú)需知道消息的來(lái)源兵睛。使用方式一般如下:
const EventEmitter = require('event');
class MyEmitter extends EventEmitter {};
const myEmitter = new MyEmitter();
myEmitter.on('event', () => {
console.log('觸發(fā)一個(gè)事件A!');
});
myEmitter.emit('event');
// 觸發(fā)一個(gè)事件A!
當(dāng)我們訂閱了'event'事件后窥浪,可以在任何地方通過(guò)emit('event')來(lái)執(zhí)行事件回調(diào)祖很,EventEmitter相當(dāng)于一個(gè)中介,負(fù)責(zé)記錄都訂閱了哪些事件并且觸發(fā)后的回調(diào)是什么寒矿,當(dāng)事件被觸發(fā)突琳,就將回調(diào)一一執(zhí)行。
發(fā)布訂閱模式
從源碼中看下EventEmitter類的是如何實(shí)現(xiàn)發(fā)布訂閱的符相。
首先我們梳理一下實(shí)現(xiàn)這個(gè)模式需要的步驟:
- 初始化空對(duì)象用存儲(chǔ)監(jiān)聽事件與對(duì)應(yīng)的回調(diào)函數(shù)
- 添加監(jiān)聽事件拆融,注冊(cè)回調(diào)函數(shù)
- 觸發(fā)事件,找出對(duì)應(yīng)回調(diào)函數(shù)隊(duì)列啊终,一一執(zhí)行
- 刪除監(jiān)聽事件
初始化空對(duì)象
在生成空對(duì)象的方式中镜豹,一般容易想到的是直接進(jìn)行賦值空對(duì)象即 var a = {};Node.js中采用的方式為var a = Object.create(null),使用這種方式理論上是應(yīng)該對(duì)對(duì)象的屬性存取的操作更快蓝牲,出于好奇作者對(duì)這兩種方式做了個(gè)粗略的對(duì)比:
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');
打印結(jié)果顯示出來(lái)貌似直接用空對(duì)象賦值與通過(guò)Object.create的方式并沒有很大的性能差異趟脂,并且還沒有誰(shuí)一定占了上風(fēng),就目前該空對(duì)象用來(lái)存儲(chǔ)注冊(cè)的監(jiān)聽事件與回調(diào)來(lái)看例衍,如果直接用{}來(lái)初始化this._events性能方面影響也許不大昔期。不過(guò)這一點(diǎn)只是個(gè)人觀點(diǎn),暫時(shí)還并不能領(lǐng)會(huì)Node里面如此運(yùn)用的深意佛玄。
添加監(jiān)聽事件硼一,注冊(cè)回調(diào)函數(shù)
EventEmitter.prototype.addListener = function addListener(type, listener) {
return _addListener(this, type, listener, false);
};
EventEmitter.prototype.on = EventEmitter.prototype.addListener;
添加監(jiān)聽者的方法為addListener,同時(shí)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);
}
}
...
}
如果之前不存在監(jiān)聽事件般贼,則會(huì)進(jìn)入第一個(gè)判斷內(nèi),其中type為事件類型,listener為觸發(fā)的事件回調(diào)哼蛆。如果之前注冊(cè)過(guò)事件蕊梧,那么回調(diào)函數(shù)會(huì)添加到回調(diào)隊(duì)列的頭或尾∪椋看如下打印結(jié)果:
myEmitter.on('event', () => {
console.log('觸發(fā)了一個(gè)事件A!');
});
myEmitter.on('event', () => {
console.log('觸發(fā)了一個(gè)事件B!');
});
myEmitter.on('talk', () => {
console.log('觸發(fā)了一個(gè)事件CS肥矢!');
// myEmitter.emit('talk');
});
console.log(myEmitter._events);
// { event: [ [function], [function] ], talk: [Function] }
myEmitter實(shí)例的_events方法就是我們存儲(chǔ)事件與回調(diào)的對(duì)象,可以看到當(dāng)我們依次注冊(cè)事件后叠洗,回調(diào)會(huì)被推到 _events對(duì)應(yīng)key的value中橄抹。
觸發(fā)事件,找出對(duì)應(yīng)回調(diào)函數(shù)隊(duì)列惕味,一一執(zhí)行
在觸發(fā)的emit函數(shù)中,會(huì)根據(jù)觸發(fā)時(shí)傳入?yún)?shù)的多少執(zhí)行不同的函數(shù):(參數(shù)不同直接執(zhí)行不同的函數(shù)玉锌,這個(gè)操作應(yīng)該會(huì)讓性能更好名挥,不過(guò)作者沒有測(cè)試這點(diǎn))
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為例看下內(nèi)部觸發(fā)實(shí)現(xiàn):
var isFn = typeof handler === 'function';
function emitMany(handler, isFn, self, args) {
if (isFn)
// handler類型為函數(shù),即對(duì)這個(gè)事件只注冊(cè)了一個(gè)監(jiān)聽函數(shù)
handler.apply(self, args);
else {
// 當(dāng)對(duì)同一事件注冊(cè)了多個(gè)監(jiān)聽函數(shù)的時(shí)候主守,handler類型為數(shù)組
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;
}
源碼中實(shí)現(xiàn)了arrayClone方法禀倔,來(lái)復(fù)制一份同樣的監(jiān)聽函數(shù),再去依次執(zhí)行副本参淫。個(gè)人對(duì)這個(gè)做法的理解是救湖,當(dāng)觸發(fā)當(dāng)前類型事件后,就鎖定需要執(zhí)行的回調(diào)函數(shù)隊(duì)列涎才,否則當(dāng)觸發(fā)回調(diào)過(guò)程中鞋既,再去推入新的回調(diào)函數(shù),或者刪除已有回調(diào)函數(shù)耍铜,容易造成不可預(yù)知的問(wèn)題邑闺。
刪除監(jiān)聽事件
如果回調(diào)事件只有一個(gè)那么直接刪除即可,如果是數(shù)組就像之前看到的那樣注冊(cè)了多組對(duì)同樣事件的監(jiān)聽棕兼,就要涉及從數(shù)組中刪除項(xiàng)的實(shí)現(xiàn)陡舅。在這里Node自己實(shí)現(xiàn)了一個(gè)spliceOne函數(shù)來(lái)代替原生的splice,并且說(shuō)明其方式比splice快1.5倍伴挚。下面是作者進(jìn)行的簡(jiǎn)易粗略靶衍,不嚴(yán)謹(jǐn)?shù)倪\(yùn)行時(shí)間比較:
上面做了一個(gè)很粗略的運(yùn)算時(shí)間比較,同樣是對(duì)長(zhǎng)度為1000的數(shù)組第100項(xiàng)進(jìn)行刪除操作茎芋,并且代碼運(yùn)行在chrome瀏覽器下(版本號(hào)61.0.3163.100)node源碼中自己實(shí)現(xiàn)的方法確實(shí)比原生的splice快了一些颅眶,不過(guò)結(jié)果只是一個(gè)參考畢竟這個(gè)對(duì)比很粗略,有興趣的童鞋可以寫一組benchmark來(lái)進(jìn)行對(duì)比败徊。