Node.js EventEmitter類源碼淺析

摘錄自: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è)模式需要的步驟:

  1. 初始化空對(duì)象用存儲(chǔ)監(jiān)聽事件與對(duì)應(yīng)的回調(diào)函數(shù)
  2. 添加監(jiān)聽事件拆融,注冊(cè)回調(diào)函數(shù)
  3. 觸發(fā)事件,找出對(duì)應(yīng)回調(diào)函數(shù)隊(duì)列啊终,一一執(zhí)行
  4. 刪除監(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');
image.png

image.png

打印結(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í)間比較:


image.png

上面做了一個(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ì)比败徊。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末帚呼,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌煤杀,老刑警劉巖眷蜈,帶你破解...
    沈念sama閱讀 212,542評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異沈自,居然都是意外死亡酌儒,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,596評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門枯途,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)忌怎,“玉大人,你說(shuō)我怎么就攤上這事酪夷×裥ィ” “怎么了?”我有些...
    開封第一講書人閱讀 158,021評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵晚岭,是天一觀的道長(zhǎng)鸥印。 經(jīng)常有香客問(wèn)我,道長(zhǎng)坦报,這世上最難降的妖魔是什么库说? 我笑而不...
    開封第一講書人閱讀 56,682評(píng)論 1 284
  • 正文 為了忘掉前任,我火速辦了婚禮片择,結(jié)果婚禮上潜的,老公的妹妹穿的比我還像新娘。我一直安慰自己字管,他們只是感情好啰挪,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,792評(píng)論 6 386
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著纤掸,像睡著了一般脐供。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上借跪,一...
    開封第一講書人閱讀 49,985評(píng)論 1 291
  • 那天政己,我揣著相機(jī)與錄音,去河邊找鬼掏愁。 笑死歇由,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的果港。 我是一名探鬼主播沦泌,決...
    沈念sama閱讀 39,107評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼辛掠!你這毒婦竟也來(lái)了谢谦?” 一聲冷哼從身側(cè)響起释牺,我...
    開封第一講書人閱讀 37,845評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎回挽,沒想到半個(gè)月后没咙,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,299評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡千劈,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,612評(píng)論 2 327
  • 正文 我和宋清朗相戀三年祭刚,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片墙牌。...
    茶點(diǎn)故事閱讀 38,747評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡涡驮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出喜滨,到底是詐尸還是另有隱情捉捅,我是刑警寧澤,帶...
    沈念sama閱讀 34,441評(píng)論 4 333
  • 正文 年R本政府宣布虽风,位于F島的核電站锯梁,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏焰情。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,072評(píng)論 3 317
  • 文/蒙蒙 一剥懒、第九天 我趴在偏房一處隱蔽的房頂上張望内舟。 院中可真熱鬧,春花似錦初橘、人聲如沸验游。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,828評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)耕蝉。三九已至,卻和暖如春夜只,著一層夾襖步出監(jiān)牢的瞬間垒在,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,069評(píng)論 1 267
  • 我被黑心中介騙來(lái)泰國(guó)打工扔亥, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留场躯,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,545評(píng)論 2 362
  • 正文 我出身青樓旅挤,卻偏偏與公主長(zhǎng)得像踢关,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子粘茄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,658評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容

  • 寫在最前 本次嘗試淺析Node.js中的EventEmitter模塊的事件機(jī)制签舞,分析在Node.js中實(shí)現(xiàn)發(fā)布訂閱...
    Annnnnn閱讀 1,334評(píng)論 0 1
  • 操作步驟1秕脓、到node官網(wǎng)(https://nodejs.org/en/)下載Node.js安裝文件,X64代表運(yùn)...
    小小程序員jh閱讀 507評(píng)論 0 4
  • npmjs.com說(shuō):npm能讓javascript開發(fā)人員發(fā)布代碼和復(fù)用代碼變得更容易.并別能非常容易的讓別人將...
    _palm閱讀 2,270評(píng)論 0 0
  • 絕文儒搭,4000漢字無(wú)一重復(fù)吠架!厲害 “只學(xué)一篇韻文便識(shí)天下漢字”,這句話說(shuō)的似乎大了师妙,但事實(shí)的確如此诵肛。 鄭州大學(xué)郭保...
    斌部t蛟閱讀 241評(píng)論 0 0
  • 昨夜,一瓢大雨默穴。 院子里的三角梅被風(fēng)吹歪了頭怔檩,委屈的窩在墻角,茶臺(tái)上落滿了玫紅色的花瓣蓄诽。順手撿拾起來(lái)薛训,可以放在透明...
    雷妮烤薯小姐閱讀 567評(píng)論 10 10