解析nodeJS模塊源碼 親手打造基于ES6的觀察者系統(tǒng)

毫無(wú)疑問(wèn)晋涣,nodeJS改變了整個(gè)前端開(kāi)發(fā)生態(tài)翔曲。本文通過(guò)分析nodeJS當(dāng)中events模塊源碼,由淺入深蛀恩,動(dòng)手實(shí)現(xiàn)了屬于自己的ES6事件觀察者系統(tǒng)。千萬(wàn)不要被nodeJS的外表嚇到茂浮,不管你是寫(xiě)nodeJS已經(jīng)輕車(chē)熟路的老司機(jī)双谆,還是初入前端的小菜鳥(niǎo)壳咕,都不妨礙對(duì)這篇文章的閱讀和理解。

事件驅(qū)動(dòng)設(shè)計(jì)理念

nodeJS官方介紹中顽馋,第二句話便是:

"Node.js uses an event-driven, non-blocking I/O model that makes it lightweight and efficient"谓厘。

由此,“事件驅(qū)動(dòng)(event-driven)”理念對(duì)nodeJS設(shè)計(jì)的重要性可見(jiàn)一斑寸谜。比如竟稳,我們對(duì)于文件的讀取,任務(wù)隊(duì)列的執(zhí)行等熊痴,都需要這樣一個(gè)觀察者模式來(lái)保障他爸。

那個(gè)最熟悉的陌生人

同時(shí),作為前端開(kāi)發(fā)人員果善,我們對(duì)于所謂的“事件驅(qū)動(dòng)”理念——即“事件發(fā)布訂閱模式(Pub/Sub模式)”一定再熟悉不過(guò)了诊笤。這種模式在js里面有與生俱來(lái)的基因。我們可以認(rèn)為JS本身就是事件驅(qū)動(dòng)型語(yǔ)言:
比如岭埠,頁(yè)面上有一個(gè)button, 點(diǎn)擊一下就會(huì)觸發(fā)上面的click事件盏混。這是因?yàn)榇藭r(shí)有特定程序正在監(jiān)聽(tīng)這個(gè)事件,隨之觸發(fā)了相關(guān)的處理程序惜论。

這個(gè)模式最大的一個(gè)好處在于能夠解耦许赃,實(shí)現(xiàn)“高內(nèi)聚、低耦合”的理念馆类。那么這樣一個(gè)“熟悉的”模式應(yīng)該怎么實(shí)現(xiàn)呢混聊?

其實(shí)社區(qū)上已經(jīng)有不少前輩的實(shí)現(xiàn)了,但是都不能算特別完美乾巧,或者不能完全符合特定的場(chǎng)景需求句喜。

本文通過(guò)解析nodeJS源碼中的events模塊,提取其精華沟于,一步步打造了一個(gè)基于ES6的eventEmitter系統(tǒng)咳胃。

讀者有任何想法,歡迎與我交流旷太。同時(shí)希望各路大神給予斧正展懈。

背景簡(jiǎn)介

為了方便大家理解,我從一個(gè)很簡(jiǎn)單的頁(yè)面實(shí)例說(shuō)起供璧。

百度某產(chǎn)品頁(yè)面中存崖,存在兩處不同的收藏組件:

  • 一處在頁(yè)面頂部;
  • 一處在頁(yè)面詳情側(cè)欄睡毒。

第一次點(diǎn)擊一個(gè)收藏組件按鈕来惧,發(fā)送異步請(qǐng)求,進(jìn)行收藏演顾,同時(shí)請(qǐng)求成功的回調(diào)函數(shù)里供搀,需要將頁(yè)面中所有“收藏”按鈕轉(zhuǎn)換狀態(tài)為“已收藏”隅居。以達(dá)到“當(dāng)前文章”收藏狀態(tài)的全局同步。

頁(yè)面實(shí)例

完成這樣的設(shè)計(jì)很簡(jiǎn)單趁曼,我們大可在業(yè)務(wù)代碼中進(jìn)行混亂的操作處理军浆,比如初學(xué)者常見(jiàn)的做法是:點(diǎn)擊第一處收藏,異步請(qǐng)求之后的回調(diào)邏輯里挡闰,修改頁(yè)面當(dāng)中所有收藏按鈕狀態(tài)。

這樣做的問(wèn)題在于耦合混亂掰盘,不僅僅是一個(gè)收藏組件摄悯,試想當(dāng)代碼中所有組件全都是這樣的“隨意”操作,后期維護(hù)成本便一發(fā)不可收愧捕。

我的Github倉(cāng)庫(kù)中奢驯,也有對(duì)于這么一個(gè)頁(yè)面實(shí)例的分析,讀者若想自己玩一下次绘,可以訪問(wèn)這里瘪阁。

當(dāng)然,更優(yōu)雅的做法就是使用事件訂閱發(fā)布系統(tǒng)邮偎。
我們先來(lái)看看nodeJS是怎么做的吧管跺!

nodeJS方案

讀者可以自己去nodeJS倉(cāng)庫(kù)查找源碼,不過(guò)更推薦參考我的Github-事件發(fā)布訂閱研究項(xiàng)目禾进,里面不僅有自己實(shí)現(xiàn)的多套基于ES6的事件發(fā)布訂閱系統(tǒng)豁跑,也“附贈(zèng)”了nodeJS實(shí)現(xiàn)源碼。同時(shí)我對(duì)源碼加上了漢語(yǔ)注釋泻云,方便大家理解艇拍。

在nodeJS中,引入eventEmitter的方式和實(shí)例化方法如下:

// 引入 events 模塊
var events = require('events');
// 創(chuàng)建 eventEmitter 對(duì)象
var eventEmitter = new events.EventEmitter();

我們要研究的宠纯,當(dāng)然就是這個(gè)eventEmitter實(shí)例卸夕。先不急于深入源碼,我們需要在使用層面先有一個(gè)清晰的理解和認(rèn)知婆瓜。不然盲目閱讀源碼快集,便極易成為一只“無(wú)頭蒼蠅”。

一個(gè)eventEmitter實(shí)例勃救,自身包含有四個(gè)屬性:

  • _events:
    這是一個(gè)object碍讨,其實(shí)相當(dāng)于一個(gè)哈希map。他用來(lái)保存一個(gè)eventEmitter實(shí)例中所有的注冊(cè)事件和事件所對(duì)應(yīng)的處理函數(shù)蒙秒。以鍵值對(duì)方式存儲(chǔ)勃黍,key為事件名;value分為兩種情況晕讲,當(dāng)當(dāng)前注冊(cè)事件只有一個(gè)注冊(cè)的監(jiān)聽(tīng)函數(shù)時(shí)覆获,value為這個(gè)監(jiān)聽(tīng)函數(shù)马澈;如果此事件有多個(gè)注冊(cè)的監(jiān)聽(tīng)函數(shù)時(shí),value值為一個(gè)數(shù)組弄息,數(shù)組每一項(xiàng)順序存儲(chǔ)了對(duì)應(yīng)此事件的注冊(cè)函數(shù)痊班。
    需要說(shuō)明的是,理解value值的這兩種情況摹量,對(duì)于后面的源碼分析非常重要涤伐。我認(rèn)為nodeJS之所以有這樣的設(shè)計(jì),是出于性能上的考慮缨称。因?yàn)楹芏嗲闆r(單一監(jiān)聽(tīng)函數(shù)情況)并不需要在內(nèi)存上新建一個(gè)額外數(shù)組凝果。

  • _eventsCount:整型,表示此eventEmitter實(shí)例中注冊(cè)的事件個(gè)數(shù)睦尽。

  • _maxListeners:整型器净,表示此eventEmitter實(shí)例中,一個(gè)事件最多所能承載的監(jiān)聽(tīng)函數(shù)個(gè)數(shù)当凡。

  • domain:在node v0.8+版本的時(shí)候山害,發(fā)布了一個(gè)模塊:domain。這個(gè)模塊做的是捕捉異步回調(diào)中出現(xiàn)的異常沿量。這里與主題無(wú)關(guān)浪慌,不做展開(kāi)。

同樣欧瘪,eventEmitter實(shí)例的構(gòu)造函數(shù)原型上眷射,包含了一些更為重要的屬性和方法,包括但不限于:

  • addListener(event, listener):
    為指定事件添加一個(gè)注冊(cè)函數(shù)(以下稱監(jiān)聽(tīng)器)到監(jiān)聽(tīng)器數(shù)組的尾部佛掖。他存在一個(gè)別名alias:on妖碉。
  • once(event, listener):
    為指定事件注冊(cè)一個(gè)單次監(jiān)聽(tīng)器,即監(jiān)聽(tīng)器最多只會(huì)觸發(fā)一次芥被,觸發(fā)后立刻解除該監(jiān)聽(tīng)器欧宜。
  • removeListener(event, listener):
    移除指定事件的某個(gè)監(jiān)聽(tīng)器,監(jiān)聽(tīng)器必須是該事件已經(jīng)注冊(cè)過(guò)的監(jiān)聽(tīng)器拴魄。
  • removeAllListeners([event]):
    移除所有事件的所有監(jiān)聽(tīng)器冗茸。如果指定事件,則移除指定事件的所有監(jiān)聽(tīng)器匹中。
  • setMaxListeners(n):
    默認(rèn)情況下夏漱,如果你添加的監(jiān)聽(tīng)器超過(guò)10個(gè)就會(huì)輸出警告信息。setMaxListeners 函數(shù)用于提高監(jiān)聽(tīng)器的默認(rèn)限制的數(shù)量顶捷。
  • listeners(event):返回指定事件的監(jiān)聽(tīng)器數(shù)組挂绰。
  • emit(event, [arg1], [arg2], [...]):
    按參數(shù)的順序執(zhí)行每個(gè)監(jiān)聽(tīng)器,如果事件有注冊(cè)監(jiān)聽(tīng)器返回true服赎,否則返回false葵蒂。

nodeJS設(shè)計(jì)之美

上一段其實(shí)簡(jiǎn)要介紹了nodeJS中eventEmitter的使用方法交播。下面,我們要做的就是深入nodeJS events模塊源碼践付,了解并學(xué)習(xí)他的設(shè)計(jì)之美秦士。

如何創(chuàng)建空對(duì)象?

我們已經(jīng)了解到永高,_events是要來(lái)儲(chǔ)存監(jiān)聽(tīng)事件(key)隧土、監(jiān)聽(tīng)器數(shù)組(value)的map。那么命爬,他的初始值一定是一個(gè)空對(duì)象次洼。直觀上,我們可以這樣創(chuàng)建一個(gè)空對(duì)象:

this._events = {};

但是nodeJS源碼中的實(shí)現(xiàn)方式卻是這樣:

function EventHandlers() {};
EventHandlers.prototype = Object.create(null);
this._events = new EventHandlers();

官方稱遇骑,這么做的原因是出于性能上的考慮,經(jīng)過(guò)jsperf比較揖曾,在v8 v4.9版本中落萎,后者性能有超出2倍的表現(xiàn)。

對(duì)此炭剪,作為一個(gè)“吹毛求疵”有態(tài)度的程序員练链,我寫(xiě)了一個(gè)benchmark,對(duì)一個(gè)對(duì)象進(jìn)行一千次取值操作奴拦,求平均時(shí)間進(jìn)行驗(yàn)證:

_events = {};
_events.test='test'
for (let i = 0; i < 1000; i++) {
    window.performance.mark('test empty object start');
    console.log(_events.test);
    window.performance.mark('test empty object end');
    window.performance.measure('test empty object','test empty object start','test empty object end');
} 
let sum1 = 0
for (let k = 0; k < 1000; k++) {
    sum1 +=window.performance.getEntriesByName('test empty object')[k].duration
}
let averge1 = sum1/1000;
console.log(averge1*1000);

function EventHandlers() {};
EventHandlers.prototype = Object.create(null);
_events = new EventHandlers();_events.test='test';
for (let i = 0; i < 1000; i++) {
    window.performance.mark('test empty object start');
    console.log(_events.test);
    window.performance.mark('test empty object end');
    window.performance.measure('test empty object','test empty object start','test empty object end');
} 
let sum1 = 0
for (let k = 0; k < 1000; k++) {
    sum1 +=window.performance.getEntriesByName('test empty object')[k].duration
}
let averge1 = sum1/1000;
console.log(averge1*1000);
  • 第一段執(zhí)行時(shí)間:111.86000000001695媒鼓;
  • 第二段執(zhí)行時(shí)間:108.37000000001353;

多執(zhí)行幾次會(huì)發(fā)現(xiàn),第一段也存在時(shí)間上短于第二段執(zhí)行時(shí)間的情況错妖÷堂總體來(lái)看,第二段時(shí)間上更短暂氯,但兩次時(shí)間比較相近潮模。

我自己的想法是,使用nodeJS源碼中這樣創(chuàng)建空對(duì)象的方式痴施,在對(duì)對(duì)象屬性的讀取上能夠節(jié)省原型鏈查找的時(shí)間擎厢。但是,如果一個(gè)屬性直接在該對(duì)象上辣吃,即hasOwnProperty()為true动遭,是否還有節(jié)省查找時(shí)間,性能優(yōu)化的空間呢神得?

另外厘惦,不同瀏覽器引擎的處理可能也存在差別,即使是流行的V8引擎循头,處理機(jī)制也“深不可測(cè)”绵估。同時(shí)炎疆,benchmark中都是對(duì)同一屬性的讀取,一般來(lái)講瀏覽器引擎對(duì)同樣的操作行為應(yīng)該會(huì)有一個(gè)“cache”機(jī)制:據(jù)我了解JIT(just-in-time)實(shí)時(shí)匯編国裳,會(huì)將重復(fù)執(zhí)行的"hot code"編譯為本地機(jī)器碼形入,極大增加效率。所以benchmark實(shí)現(xiàn)的purity也有被一定程度的干擾缝左。不過(guò)好在測(cè)試實(shí)例都是在相同環(huán)境下執(zhí)行亿遂。

所以源碼中,此處性能優(yōu)化上的2倍數(shù)值渺杉,我持一定的保留態(tài)度蛇数。

addListener實(shí)現(xiàn)

經(jīng)過(guò)整理,適當(dāng)刪減后的源碼點(diǎn)擊這里查看是越,保留了我的注釋耳舅。我們來(lái)一步一步解讀下源碼。

判斷添加的監(jiān)聽(tīng)器是否為函數(shù)類型倚评,使用了typeof進(jìn)行驗(yàn)證:

if (typeof listener !== 'function') {
    throw new TypeError('"listener" argument must be a function');
}

接下來(lái)浦徊,要分為幾種情況。
case1:
判斷_events表是否已經(jīng)存在天梧,如果不存在盔性,則說(shuō)明是第一次為eventEmitter實(shí)例添加事件和監(jiān)聽(tīng)器,需要新創(chuàng)建_events:

if (!events) {
    events = target._events = new EventHandlers();
    target._eventsCount = 0;
} 

還記得EventHandlers是什么嗎呢岗?忘記了把屏幕往上滾動(dòng)再看一下吧冕香。

同時(shí),添加指定的事件和此事件對(duì)應(yīng)的監(jiān)聽(tīng)器:

existing = events[type] = listener;
++target._eventsCount;

注意第一次創(chuàng)建時(shí)后豫,為了節(jié)省內(nèi)存悉尾,提高性能,events[type]值是一個(gè)監(jiān)聽(tīng)器函數(shù)硬贯。如果再次為相同的events[type]添加監(jiān)聽(tīng)器時(shí)(下面case2)焕襟,events[type]對(duì)應(yīng)的值需要變成一個(gè)數(shù)組來(lái)存儲(chǔ)。

case2:
又啰嗦一遍:如果_events已存在饭豹,在為相關(guān)事件添加監(jiān)聽(tīng)器時(shí)鸵赖,需要判斷events[type]是函數(shù)類型(只存在一個(gè)監(jiān)聽(tīng)函數(shù))還是已經(jīng)成為了一個(gè)數(shù)組類型(已經(jīng)存在一個(gè)以上監(jiān)聽(tīng)函數(shù))。
并且根據(jù)相關(guān)參數(shù)prepend拄衰,分為監(jiān)聽(tīng)器數(shù)組頭部插入和尾部插入兩種情況它褪,以保證監(jiān)聽(tīng)器的順序執(zhí)行:

if (typeof existing === 'function') {
    existing = events[type] = prepend ? [listener, existing] :
                                      [existing, listener];
} 
else {
    if (prepend) {
        existing.unshift(listener);
    } 
    else {
        existing.push(listener);
    }
}

case3:
在閱讀源碼時(shí),我還發(fā)現(xiàn)了一個(gè)很“詭異”的邏輯:

 if (events.newListener) {
    target.emit('newListener', type,
              listener.listener ? listener.listener : listener);
    events = target._events;
}
existing = events[type];

仔細(xì)分析翘悉,他的目的是因?yàn)閚odeJS默認(rèn):當(dāng)所有的eventEmitter對(duì)象在添加新的監(jiān)聽(tīng)函數(shù)時(shí)茫打,都會(huì)發(fā)出newListener事件。這其實(shí)也并不奇怪,我個(gè)人認(rèn)為這么設(shè)計(jì)還是非常合理的老赤。

cae4:
之前介紹了我們可以設(shè)置一個(gè)事件對(duì)應(yīng)的最大監(jiān)聽(tīng)器個(gè)數(shù)轮洋,nodeJS源碼中通過(guò)這樣的代碼來(lái)實(shí)現(xiàn):

EventEmitter.prototype.setMaxListeners = function setMaxListeners(n) {
    if (typeof n !== 'number' || n < 0 || isNaN(n)) {
        throw new TypeError('"n" argument must be a positive number');
    }
    this._maxListeners = n;
    return this;
};

當(dāng)對(duì)這個(gè)值進(jìn)行了設(shè)置之后,如果超過(guò)此閾值抬旺,將會(huì)進(jìn)行報(bào)警:

if (!existing.warned) {
    m = $getMaxListeners(target);
    if (m && m > 0 && existing.length > m) {
        existing.warned = true;
        const w = new Error('Possible EventEmitter memory leak detected. ' +
                            `${existing.length} ${String(type)} listeners ` +
                            'added. Use emitter.setMaxListeners() to ' +
                            'increase limit');
        w.name = 'MaxListenersExceededWarning';
        w.emitter = target;
        w.type = type;
        w.count = existing.length;
        process.emitWarning(w);
    }
}

emit發(fā)射器實(shí)現(xiàn)

有了之前的注冊(cè)監(jiān)聽(tīng)器過(guò)程弊予,那么我們?cè)賮?lái)看看監(jiān)聽(tīng)器是如何被觸發(fā)的。其實(shí)觸發(fā)過(guò)程直觀上并不難理解开财,核心思想就是將監(jiān)聽(tīng)器數(shù)組中的每一項(xiàng)汉柒,即監(jiān)聽(tīng)函數(shù)逐個(gè)執(zhí)行就好了。

經(jīng)過(guò)整理责鳍,適當(dāng)刪減后的源碼同樣可以這里找到碾褂。源碼中,包含了較多的錯(cuò)誤信息處理內(nèi)容历葛,忽略不表正塌。下面我挑出一些“出神入化”的細(xì)節(jié)來(lái)分析。

首先恤溶,有了上面的分析传货,我們現(xiàn)在可以清晰的意識(shí)到某個(gè)事件的監(jiān)聽(tīng)處理可能是一個(gè)函數(shù)類型,表示該事件只有一個(gè)事件處理程序宏娄;也可能是個(gè)數(shù)組,表示該事件有多個(gè)事件處理程序逮壁,存儲(chǔ)在監(jiān)聽(tīng)器數(shù)組中孵坚。(我又啰嗦了一遍,因?yàn)槔斫膺@個(gè)太重要了窥淆,不然你會(huì)看暈的)

同時(shí)卖宠,emit方法可以接受多個(gè)參數(shù)。第一個(gè)參數(shù)為事件類型:type忧饭,下面兩行代碼用于獲取某個(gè)事件的監(jiān)聽(tīng)處理類型扛伍。用isFn布爾值來(lái)表示。

handler = events[type];
var isFn = typeof handler === 'function';

isFn為true词裤,表示該事件只有一個(gè)監(jiān)聽(tīng)函數(shù)刺洒。否則,存在多個(gè)吼砂,儲(chǔ)存在數(shù)組中逆航。

源碼中對(duì)于emit參數(shù)個(gè)數(shù)有判斷,并進(jìn)行了switch分支處理:

switch (len) {
    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);
}

我們挑一個(gè)相對(duì)最復(fù)雜的看一下——默認(rèn)模式調(diào)用的emitMany:

function emitMany(handler, isFn, self, args) {
    if (isFn) {
        handler.apply(self, args);
    }
    else {
        var len = handler.length;
        var listeners = arrayClone(handler, len);
        for (var i = 0; i < len; ++i) {
            listeners[i].apply(self, args);
        }
    }
}

對(duì)于只有一個(gè)事件處理程序的情況(isFn為true)渔肩,直接執(zhí)行:

handler.apply(self, args);

否則因俐,便使用for循環(huán),逐個(gè)調(diào)用:

listeners[i].apply(self, args);

非常有意思的一個(gè)細(xì)節(jié)在于:

var listeners = arrayClone(handler, len);

這里需要讀者細(xì)心體會(huì)。

源碼讀到這里抹剩,我不禁要感嘆設(shè)計(jì)的嚴(yán)謹(jǐn)精妙之處撑帖。上面代碼處理的意義在于:防止在一個(gè)事件監(jiān)聽(tīng)器中監(jiān)聽(tīng)同一個(gè)事件,從而導(dǎo)致死循環(huán)的出現(xiàn)澳眷。
如果您不理解胡嘿,且看我這個(gè)例子:

let emitter = new eventEmitter;
emitter.on('message1', function test () {
    // some codes here
    // ...
    emitter.on('message1', test}
});
emit('message1');

講道理,正常來(lái)講境蔼,不經(jīng)過(guò)任何處理灶平,上述代碼在事件處理程序內(nèi)部又添加了對(duì)于同一個(gè)事件的監(jiān)聽(tīng),這必然會(huì)帶來(lái)死循環(huán)問(wèn)題箍土。
因?yàn)樵趀mit執(zhí)行處理程序的時(shí)候逢享,我們又向監(jiān)聽(tīng)器隊(duì)列添加了一項(xiàng)。這一項(xiàng)執(zhí)行時(shí)吴藻,又會(huì)“子子孫孫無(wú)窮匱也”的向監(jiān)聽(tīng)器數(shù)組尾部添加瞒爬。

源碼中對(duì)于這個(gè)問(wèn)題的解決方案是:在執(zhí)行emit方法時(shí),使用arrayClone方法拷貝出另一個(gè)一模一樣的數(shù)組沟堡,進(jìn)而執(zhí)行它侧但。這樣一來(lái),當(dāng)我們?cè)诒O(jiān)聽(tīng)器內(nèi)監(jiān)聽(tīng)同一個(gè)事件時(shí)航罗,的確給原監(jiān)聽(tīng)器數(shù)組添加了新的監(jiān)聽(tīng)函數(shù)禀横,但并沒(méi)有影響到當(dāng)前這個(gè)被拷貝出來(lái)的副本數(shù)組。在循環(huán)中粥血,我們執(zhí)行的也是這個(gè)副本函數(shù)柏锄。

單次監(jiān)聽(tīng)器once實(shí)現(xiàn)

once(event, listener)是為指定事件注冊(cè)一個(gè)單次事件處理程序,即監(jiān)聽(tīng)器最多只會(huì)觸發(fā)一次复亏,觸發(fā)后立刻解除該監(jiān)聽(tīng)器趾娃。

實(shí)現(xiàn)方式主要是在進(jìn)行監(jiān)聽(tīng)器綁定時(shí),對(duì)于監(jiān)聽(tīng)函數(shù)進(jìn)行一層包裝缔御。該包裝方式在原有函數(shù)上添加一個(gè)flag標(biāo)識(shí)位抬闷,并在觸發(fā)監(jiān)聽(tīng)函數(shù)前就調(diào)用removeListener()方法,除掉此監(jiān)聽(tīng)函數(shù)耕突。我理解笤成,這是一種“雙保險(xiǎn)”的體現(xiàn)。

代碼里眷茁,我們可以抽絲剝繭(已進(jìn)行刪減)學(xué)習(xí)一下:

 EventEmitter.prototype.once = function once(type, listener) {
    this.on(type, _onceWrap(this, type, listener));
    return this;
};

once方法調(diào)用on方法(即addListener方法疹启,on為別名),第二個(gè)參數(shù)即監(jiān)聽(tīng)程序進(jìn)行_onceWrap化包裝蔼卡,包裝過(guò)程為:

this.target.removeListener(this.type, this.wrapFn);
if (!this.fired) {
    this.fired = true;
    this.listener.apply(this.target, arguments);
}

_onceWrap化的主要思想是將once第二個(gè)參數(shù)listener的執(zhí)行喊崖,包上了一次判斷挣磨,并在執(zhí)行前進(jìn)行removeListener刪除該監(jiān)聽(tīng)程序。:

 this.listener.apply(this.target, arguments);

removeListener的驚鴻一瞥

removeListener(type, listener)移除指定事件的某個(gè)監(jiān)聽(tīng)器荤懂。其實(shí)這個(gè)實(shí)現(xiàn)思路也比較容易理解茁裙,我們已經(jīng)知道events[type]可能是函數(shù)類型,也可能是數(shù)組類型节仿。如果是數(shù)組類型晤锥,只需要進(jìn)行遍歷,找到相關(guān)的監(jiān)聽(tīng)器進(jìn)行刪除就可以了廊宪。

不過(guò)關(guān)鍵問(wèn)題就在于對(duì)數(shù)組項(xiàng)的刪除矾瘾。

平時(shí)開(kāi)發(fā),我們常用splice進(jìn)行數(shù)組中某一項(xiàng)的刪除箭启,99%的case都會(huì)想到這個(gè)方法壕翩。可是nodeJS相關(guān)源碼中傅寡,對(duì)于刪除進(jìn)行了優(yōu)化放妈。自己封裝了一個(gè)spliceOne方法,用于刪除數(shù)組中指定角標(biāo)荐操。并且號(hào)稱這個(gè)方法比使用splice要快1.5倍芜抒。我們就來(lái)看一下他是如何實(shí)現(xiàn)的:

function spliceOne(list, index) {
    for (var i = index, k = i + 1, n = list.length; k < n; i += 1, k += 1) {
        list[i] = list[k];
    }
    list.pop();
}

傳統(tǒng)刪除方法:

list.splice(index, 1);

究竟是否計(jì)算更快,我也實(shí)現(xiàn)了一個(gè)benchmark托启,產(chǎn)生長(zhǎng)度為1000的數(shù)組宅倒,刪除其第52項(xiàng)。反復(fù)執(zhí)行1000次求平均耗時(shí):

let arr = Array.from(Array(100).keys());
for (let i = 0; i < 1000; i++) {
    window.performance.mark('test splice start');
    arr.splice(52, 1);
    window.performance.mark('test splice end');
    window.performance.measure('test splice','test splice start','test splice end');
}
let sum1 = 0
for (let k = 0; k < 1000; k++) {
    sum1 +=window.performance.getEntriesByName('test splice')[k].duration
}
let averge1 = sum1/1000;
console.log(averge1*1000); // 1.7749999999869034


let arr = Array.from(Array(100).keys());
for (let i = 0; i < 1000; i++) {
    window.performance.mark('test splice start');
    spliceOne(arr, 52);
    window.performance.mark('test splice end');
    window.performance.measure('test splice','test splice start','test splice end');
}
let sum1 = 0
for (let k = 0; k < 1000; k++) {
    sum1 +=window.performance.getEntriesByName('test splice')[k].duration
}
let averge1 = sum1/1000;
console.log(averge1*1000); // 1.5350000000089494
  • 第一段執(zhí)行時(shí)間:1.7749999999869034屯耸;
  • 第二段執(zhí)行時(shí)間:1.5350000000089494;

明顯使用spliceOne方法更快唉堪,時(shí)間上縮短了13.5%,不過(guò)依然沒(méi)有達(dá)到官方的1.5肩民,需要說(shuō)明的是我采用最新版本的Chrome進(jìn)行測(cè)試。

自己造輪子

前文我們感受了nodeJS中的eventEmitter實(shí)現(xiàn)方式链方。我也對(duì)于其中的核心方法持痰,在源碼層面進(jìn)行了剖析。學(xué)習(xí)到了“精華”之后祟蚀,更重要的要學(xué)以致用工窍,自己實(shí)現(xiàn)一個(gè)基于ES6的事件發(fā)布訂閱系統(tǒng)。

我的實(shí)現(xiàn)版本中充分利用了ES6語(yǔ)法特性前酿,并且相對(duì)于nodeJS實(shí)現(xiàn)減少了一些“不必要的”優(yōu)化和判斷患雏。

因?yàn)閚odeJS的實(shí)現(xiàn)中,很多api在前端瀏覽器環(huán)境開(kāi)發(fā)中并用不到罢维。所以我對(duì)對(duì)外暴露的方法進(jìn)行了精簡(jiǎn)淹仑。最終實(shí)現(xiàn)上,除去注釋部分,只用了不到40行代碼匀借。如果您有興趣颜阐,可以去代碼倉(cāng)庫(kù)訪問(wèn),整個(gè)邏輯還是很簡(jiǎn)單的吓肋。

里面同時(shí)附贈(zèng)了我同事@顏海鏡大神基于zepto實(shí)現(xiàn)版本凳怨,以及nodeJS events模塊源碼,方便讀者進(jìn)行對(duì)比是鬼。
整個(gè)過(guò)程編寫(xiě)時(shí)間倉(cāng)促肤舞,其中必然不乏疏漏之處,還請(qǐng)您斧正并與我討論均蜜。

總結(jié)

對(duì)于nodeJS源碼events模塊的閱讀李剖,令我受益匪淺。設(shè)計(jì)層面上兆龙,優(yōu)秀的包裝和抽象思路對(duì)我一定的啟發(fā)杖爽;實(shí)現(xiàn)層面上,很多“意想不到”的case處理紫皇,讓我“嘆為觀止”慰安。

雖然業(yè)務(wù)上暫時(shí)使用不到nodeJS,但是對(duì)于每一個(gè)前端開(kāi)發(fā)人員來(lái)說(shuō)聪铺,這樣的學(xué)習(xí)我認(rèn)為是有必要的化焕。今后,我會(huì)整理出文章铃剔,總結(jié)對(duì)nodeJS源碼更多模塊的分析撒桨,希望同讀者能夠保持交流和探討。

整篇文章里面列出的benchmark键兜,我認(rèn)為并不完美凤类。同時(shí),對(duì)于瀏覽器引擎處理上普气,我存在知識(shí)盲點(diǎn)和漏洞谜疤,希望有大神給與斧正。

PS:百度知識(shí)搜索部大前端繼續(xù)招兵買(mǎi)馬现诀,有意向者火速聯(lián)系夷磕。。仔沿。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末奋刽,一起剝皮案震驚了整個(gè)濱河市省容,隨后出現(xiàn)的幾起案子琳状,更是在濱河造成了極大的恐慌,老刑警劉巖膘螟,帶你破解...
    沈念sama閱讀 221,635評(píng)論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異抖坪,居然都是意外死亡萍鲸,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評(píng)論 3 399
  • 文/潘曉璐 我一進(jìn)店門(mén)擦俐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)脊阴,“玉大人,你說(shuō)我怎么就攤上這事蚯瞧『倨冢” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 168,083評(píng)論 0 360
  • 文/不壞的土叔 我叫張陵埋合,是天一觀的道長(zhǎng)备徐。 經(jīng)常有香客問(wèn)我,道長(zhǎng)甚颂,這世上最難降的妖魔是什么蜜猾? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 59,640評(píng)論 1 296
  • 正文 為了忘掉前任,我火速辦了婚禮振诬,結(jié)果婚禮上蹭睡,老公的妹妹穿的比我還像新娘。我一直安慰自己赶么,他們只是感情好肩豁,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評(píng)論 6 397
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著辫呻,像睡著了一般清钥。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上放闺,一...
    開(kāi)封第一講書(shū)人閱讀 52,262評(píng)論 1 308
  • 那天祟昭,我揣著相機(jī)與錄音,去河邊找鬼怖侦。 笑死篡悟,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的础钠。 我是一名探鬼主播,決...
    沈念sama閱讀 40,833評(píng)論 3 421
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼叉谜,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼旗吁!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起停局,我...
    開(kāi)封第一講書(shū)人閱讀 39,736評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤很钓,失蹤者是張志新(化名)和其女友劉穎香府,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體码倦,經(jīng)...
    沈念sama閱讀 46,280評(píng)論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡企孩,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評(píng)論 3 340
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了袁稽。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片勿璃。...
    茶點(diǎn)故事閱讀 40,503評(píng)論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖推汽,靈堂內(nèi)的尸體忽然破棺而出补疑,到底是詐尸還是另有隱情,我是刑警寧澤歹撒,帶...
    沈念sama閱讀 36,185評(píng)論 5 350
  • 正文 年R本政府宣布莲组,位于F島的核電站,受9級(jí)特大地震影響暖夭,放射性物質(zhì)發(fā)生泄漏锹杈。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評(píng)論 3 333
  • 文/蒙蒙 一迈着、第九天 我趴在偏房一處隱蔽的房頂上張望竭望。 院中可真熱鬧,春花似錦寥假、人聲如沸市框。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 32,340評(píng)論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)枫振。三九已至,卻和暖如春萤彩,著一層夾襖步出監(jiān)牢的瞬間粪滤,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,460評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工雀扶, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杖小,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,909評(píng)論 3 376
  • 正文 我出身青樓愚墓,卻偏偏與公主長(zhǎng)得像予权,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子浪册,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評(píng)論 2 359

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

  • https://nodejs.org/api/documentation.html 工具模塊 Assert 測(cè)試 ...
    KeKeMars閱讀 6,339評(píng)論 0 6
  • 本學(xué)習(xí)筆記是根據(jù)《Node.js開(kāi)發(fā)指南》一書(shū)進(jìn)行學(xué)習(xí)扫腺。 全局對(duì)象 JavaScript中有一個(gè)特殊的對(duì)象,稱為全...
    秋意思寒閱讀 1,369評(píng)論 0 2
  • 內(nèi)容來(lái)自《Node.js開(kāi)發(fā)指南》 核心模塊是 Node.js 的心臟村象,它由一些精簡(jiǎn)而高效的庫(kù)組成笆环,為 Node....
    angelwgh閱讀 900評(píng)論 0 1
  • Node.js EventEmitter Node.js 所有的異步 I/O 操作在完成時(shí)都會(huì)發(fā)送一個(gè)事件到事件隊(duì)...
    FTOLsXD閱讀 318評(píng)論 1 2
  • 近日有位親戚送了一箱泡面給我攒至,差不多四年沒(méi)有吃「出前一丁」,忽然發(fā)現(xiàn)忘了味道躁劣,便泡了一包吃迫吐。嘗到第一口的時(shí)候發(fā)現(xiàn)和...
    Ubuay閱讀 555評(píng)論 6 1