GitHub地址: JavaScript EventEmitter博客地址:JavaScript EventEmitterjavascript
水平有限,歡迎批評指正html
2個多月前把 Github 上的 eventemitter3 和 Node.js
下的事件模塊 events 的源碼抄了一遍,才終於對 JavaScript
事件有所瞭解。java
上個週末花點時間根據以前看源碼的理解本身用 ES6 實現了一個 eventemitter8,而後也發佈到 npm 上了,讓我比較意外的是才發佈兩天在沒有 readme
介紹,沒有任何宣傳的狀況下竟然有45個下載,我很好奇都是誰下載的,會不會用。我花了很多時間半抄半原創的一個 JavaScript
時間處理庫 now.js (npm
傳送門:now.js) ,在我大力宣傳的狀況下,4個月的下載量才177。真是有心栽花花不開,無意插柳柳成蔭
!node
eventemitter8
大部分是我根據看源碼理解後寫出來的,有一些方法如listeners
,listenerCount
和 eventNames
一會兒想不起來到底作什麼,回頭重查。測試用例很多是參考了 eventemitter3
,在此對 eventemitter3
的開發者們和 Node.js
事件模塊的開發者們表示感謝!git
下面來說講我對 JavaScript
事件的理解:github
從上圖能夠看出,JavaScript
事件最核心的包括事件監聽 (addListener)
、事件觸發 (emit)
、事件刪除 (removeListener)
。面試
首先,監聽確定要有監聽的目標,或者說是對象,那爲了達到區分目標的目的,名字是不可少的,咱們定義爲 type
。npm
其次,監聽的目標必定要有某種動做,對應到 JavaScript
裏實際上就是某種方法,這裏定義爲 fn
。數組
譬如能夠監聽一個 type
爲 add
,方法爲某一個變量 a
值加1
的方法 fn = () => a + 1
的事件。若是咱們還想監聽一個使變量 b
加2
的方法,咱們第一反應多是建立一個 type
爲 add2
,方法 爲 fn1 = () => b + 2
的事件。你可能會想,這太浪費了,我能不能只監聽一個名字,讓它執行多於一個方法的事件。固然是能夠的。瀏覽器
那麼怎麼作呢?
很簡單,把監聽的方法放在一個數組裏,遍歷數組順序執行就能夠了。以上例子變爲 type
爲 add
,方法爲[fn, fn1]
。
若是要細分的話還能夠分爲能夠無限次執行的事件 on
和 只容許執行一次的事件 once
(執行完後當即將事件刪除)。待後詳述。
單有事件監聽是不夠的,必需要有事件觸發才能算完成整個過程。emit
就是去觸發監聽的特定 type
對應的單個事件或者一系列事件。拿前面的例子來講單個事件就是去執行 fn
,一系列事件就是去遍歷執行 fn
和 fn1
。
嚴格意義上來說,事件監聽和事件觸發已經能完成整個過程。事件刪除無關緊要。但不少時候,咱們仍是須要事件刪除的。好比前面講的只容許執行一次事件 once
,若是不提供刪除方法,很難保證你何時會再次執行它。一般狀況下,只要是再也不須要的事件,咱們都應該去刪除它。
核心部分講完,下面簡單的對 eventemitter8
的源碼進行解析。
所有源碼:
const toString = Object.prototype.toString; const isType = obj => toString.call(obj).slice(8, -1).toLowerCase(); const isArray = obj => Array.isArray(obj) || isType(obj) === 'array'; const isNullOrUndefined = obj => obj === null || obj === undefined; const _addListener = function(type, fn, context, once) { if (typeof fn !== 'function') { throw new TypeError('fn must be a function'); } fn.context = context; fn.once = !!once; const event = this._events[type]; // only one, let `this._events[type]` to be a function if (isNullOrUndefined(event)) { this._events[type] = fn; } else if (typeof event === 'function') { // already has one function, `this._events[type]` must be a function before this._events[type] = [event, fn]; } else if (isArray(event)) { // already has more than one function, just push this._events[type].push(fn); } return this; }; class EventEmitter { constructor() { if (this._events === undefined) { this._events = Object.create(null); } } addListener(type, fn, context) { return _addListener.call(this, type, fn, context); } on(type, fn, context) { return this.addListener(type, fn, context); } once(type, fn, context) { return _addListener.call(this, type, fn, context, true); } emit(type, ...rest) { if (isNullOrUndefined(type)) { throw new Error('emit must receive at lease one argument'); } const events = this._events[type]; if (isNullOrUndefined(events)) return false; if (typeof events === 'function') { events.call(events.context || null, rest); if (events.once) { this.removeListener(type, events); } } else if (isArray(events)) { events.map(e => { e.call(e.context || null, rest); if (e.once) { this.removeListener(type, e); } }); } return true; } removeListener(type, fn) { if (isNullOrUndefined(this._events)) return this; // if type is undefined or null, nothing to do, just return this if (isNullOrUndefined(type)) return this; if (typeof fn !== 'function') { throw new Error('fn must be a function'); } const events = this._events[type]; if (typeof events === 'function') { events === fn && delete this._events[type]; } else { const findIndex = events.findIndex(e => e === fn); if (findIndex === -1) return this; // match the first one, shift faster than splice if (findIndex === 0) { events.shift(); } else { events.splice(findIndex, 1); } // just left one listener, change Array to Function if (events.length === 1) { this._events[type] = events[0]; } } return this; } removeAllListeners(type) { if (isNullOrUndefined(this._events)) return this; // if not provide type, remove all if (isNullOrUndefined(type)) this._events = Object.create(null); const events = this._events[type]; if (!isNullOrUndefined(events)) { // check if `type` is the last one if (Object.keys(this._events).length === 1) { this._events = Object.create(null); } else { delete this._events[type]; } } return this; } listeners(type) { if (isNullOrUndefined(this._events)) return []; const events = this._events[type]; // use `map` because we need to return a new array return isNullOrUndefined(events) ? [] : (typeof events === 'function' ? [events] : events.map(o => o)); } listenerCount(type) { if (isNullOrUndefined(this._events)) return 0; const events = this._events[type]; return isNullOrUndefined(events) ? 0 : (typeof events === 'function' ? 1 : events.length); } eventNames() { if (isNullOrUndefined(this._events)) return []; return Object.keys(this._events); } } export default EventEmitter;
代碼不多,只有151行,由於寫的簡單版,且用的 ES6
,因此才這麼少;Node.js
的事件和 eventemitter3
可比這多且複雜很多,有興趣可自行深刻研究。
const toString = Object.prototype.toString; const isType = obj => toString.call(obj).slice(8, -1).toLowerCase(); const isArray = obj => Array.isArray(obj) || isType(obj) === 'array'; const isNullOrUndefined = obj => obj === null || obj === undefined;
這4行就是一些工具函數,判斷所屬類型、判斷是不是 null
或者 undefined
。
constructor() { if (isNullOrUndefined(this._events)) { this._events = Object.create(null); } }
建立了一個 EventEmitter
類,而後在構造函數裏初始化一個類的 _events
屬性,這個屬性不須要要繼承任何東西,因此用了 Object.create(null)
。固然這裏 isNullOrUndefined(this._events)
還去判斷了一下 this._events
是否爲 undefined
或者 null
,若是是才須要建立。但這不是必要的,由於實例化一個 EventEmitter
都會調用構造函數,皆爲初始狀態,this._events
應該是不可能已經定義了的,可去掉。
addListener(type, fn, context) { return _addListener.call(this, type, fn, context); } on(type, fn, context) { return this.addListener(type, fn, context); } once(type, fn, context) { return _addListener.call(this, type, fn, context, true); }
接下來是三個方法 addListener
、on
、once
,其中 on
是 addListener
的別名,可執行屢次。once
只能執行一次。
三個方法都用到了 _addListener
方法:
const _addListener = function(type, fn, context, once) { if (typeof fn !== 'function') { throw new TypeError('fn must be a function'); } fn.context = context; fn.once = !!once; const event = this._events[type]; // only one, let `this._events[type]` to be a function if (isNullOrUndefined(event)) { this._events[type] = fn; } else if (typeof event === 'function') { // already has one function, `this._events[type]` must be a function before this._events[type] = [event, fn]; } else if (isArray(event)) { // already has more than one function, just push this._events[type].push(fn); } return this; };
方法有四個參數,type
是監聽事件的名稱,fn
是監聽事件對應的方法,context
俗稱爸爸
,改變 this
指向的,也就是執行的主體。once
是一個布爾型,用來標誌是否只執行一次。
首先判斷 fn
的類型,若是不是方法,拋出一個類型錯誤。fn.context = context;fn.once = !!once
把執行主體和是否執行一次做爲方法的屬性。const event = this._events[type]
把該對應 type
的全部已經監聽的方法存到變量 event
。
// only one, let `this._events[type]` to be a function if (isNullOrUndefined(event)) { this._events[type] = fn; } else if (typeof event === 'function') { // already has one function, `this._events[type]` must be a function before this._events[type] = [event, fn]; } else if (isArray(event)) { // already has more than one function, just push this._events[type].push(fn); } return this;
若是 type
自己沒有正在監放任何方法,this._events[type] = fn
直接把監聽的方法 fn
賦給 type
屬性 ;若是正在監聽一個方法,則把要添加的 fn
和以前的方法變成一個含有2個元素的數組 [event, fn]
,而後再賦給 type
屬性,若是正在監聽超過2個方法,直接 push
便可。最後返回 this
,也就是 EventEmitter
實例自己。
簡單來說不論是監聽多少方法,都放到數組裏是不必像上面細分。但性能較差,只有一個方法時 key: fn
的效率比 key: [fn]
要高。
再回頭看看三個方法:
addListener(type, fn, context) { return _addListener.call(this, type, fn, context); } on(type, fn, context) { return this.addListener(type, fn, context); } once(type, fn, context) { return _addListener.call(this, type, fn, context, true); }
addListener
須要用 call
來改變 this
指向,指到了類的實例。once
則多傳了一個標誌位 true
來標誌它只須要執行一次。這裏你會看到我在 addListener
並無傳 false
做爲標誌位,主要是由於我懶,但並不會影響到程序的邏輯。由於前面的 fn.once = !!once
已經能很好的處理不傳值的狀況。沒傳值 !!once
爲 false
。
接下來說 emit
emit(type, ...rest) { if (isNullOrUndefined(type)) { throw new Error('emit must receive at lease one argument'); } const events = this._events[type]; if (isNullOrUndefined(events)) return false; if (typeof events === 'function') { events.call(events.context || null, rest); if (events.once) { this.removeListener(type, events); } } else if (isArray(events)) { events.map(e => { e.call(e.context || null, rest); if (e.once) { this.removeListener(type, e); } }); } return true; }
事件觸發須要指定具體的 type
不然直接拋出錯誤。這個很容易理解,你都沒有指定名稱,我怎麼知道該去執行誰的事件。if (isNullOrUndefined(events)) return false
,若是 type
對應的方法是 undefined
或者 null
,直接返回 false
。由於壓根沒有對應 type
的方法能夠執行。而 emit
須要知道是否被成功觸發。
接着判斷 evnts
是否是一個方法,若是是, events.call(events.context || null, rest)
執行該方法,若是指定了執行主體,用 call
改變 this
的指向指向 events.context
主體,不然指向 null
,全局環境。對於瀏覽器環境來講就是 window
。差點忘了 rest
,rest
是方法執行時的其餘參數變量,能夠不傳,也能夠爲一個或多個。執行結束後判斷 events.once
,若是爲 true
,就用 removeListener
移除該監聽事件。
若是 evnts
是數組,邏輯同樣,只是須要遍歷數組去執行全部的監聽方法。
成功執行結束後返回 true
。
removeListener(type, fn) { if (isNullOrUndefined(this._events)) return this; // if type is undefined or null, nothing to do, just return this if (isNullOrUndefined(type)) return this; if (typeof fn !== 'function') { throw new Error('fn must be a function'); } const events = this._events[type]; if (typeof events === 'function') { events === fn && delete this._events[type]; } else { const findIndex = events.findIndex(e => e === fn); if (findIndex === -1) return this; // match the first one, shift faster than splice if (findIndex === 0) { events.shift(); } else { events.splice(findIndex, 1); } // just left one listener, change Array to Function if (events.length === 1) { this._events[type] = events[0]; } } return this; }
removeListener
接收一個事件名稱 type
和一個將要被移除的方法 fn
。if (isNullOrUndefined(this._events)) return this
這裏表示若是 EventEmitter
實例自己的 _events
爲 null
或者 undefined
的話,沒有任何事件監聽,直接返回 this
。
if (isNullOrUndefined(type)) return this
若是沒有提供事件名稱,也直接返回 this
。
if (typeof fn !== 'function') { throw new Error('fn must be a function'); }
fn
若是不是一個方法,直接拋出錯誤,很好理解。
接着判斷 type
對應的 events
是否是一個方法,是,而且 events === fn
說明 type
對應的方法有且僅有一個,等於咱們指定要刪除的方法。這個時候 delete this._events[type]
直接刪除掉 this._events
對象裏 type
便可。
全部的 type
對應的方法都被移除後。想想 this._events[type] = undefined
和 delete this._events[type]
會有什麼不一樣?
差別是很大的,this._events[type] = undefined
僅僅是將 this._events
對象裏的 type
屬性賦值爲 undefined
,type
這一屬性依然佔用內存空間,但其實已經沒什麼用了。若是這樣的 type
一多,有可能形成內存泄漏。delete this._events[type]
則直接刪除,不佔內存空間。前者也是 Node.js
事件模塊和 eventemitter3
早期實現的作法。
若是 events
是數組,這裏我沒有用 isArray
進行判斷,而是直接用一個 else
,緣由是 this._events[type]
的輸入限制在 on
或者 once
中,而它們已經限制了 this._events[type]
只能是方法組成的數組或者是一個方法,最多加上不當心或者人爲賦成 undefined
或 null
的狀況,但這個狀況咱們也在前面判斷過了。
由於 isArray
這個工具方法其實運行效率是不高的,爲了追求一些效率,在不影響運行邏輯狀況下能夠不用 isArray
。並且 typeof events === 'function'
用 typeof
判斷方法也比 isArray
的效率要高,這也是爲何不先判斷是不是數組的緣由。用 typeof
去判斷一個方法也比 Object.prototype.toSting.call(events) === '[object Function]
效率要高。但數組不能用 typeof
進行判斷,由於返回的是 object
, 這衆所周知。雖然如此,在我面試過的不少人中,仍然有不少人不知道。。。
const findIndex = events.findIndex(e => e === fn)
此處用 ES6
的數組方法 findIndex
直接去查找 fn
在 events
中的索引。若是 findIndex === -1
說明咱們沒有找到要刪除的 fn
,直接返回 this
就好。若是 findIndex === 0
,是數組第一個元素,shift
剔除,不然用 splice
剔除。由於 shift
比 splice
效率高。
findIndex
的效率其實沒有 for
循環去查找的高,因此 eventemitter8
的效率在我沒有作 benchmark
以前我就知道確定會比 eventemitter3
效率要低很多。不那麼追求執行效率時固然是用最懶的方式來寫最爽。所謂的懶即正義
。。。
最後還得判斷移除 fn
後 events
剩餘的數量,若是隻有一個,基於以前要作的優化,this._events[type] = events[0]
把含有一個元素的數組變成一個方法,降維打擊一下。。。
最後的最後 return this
返回自身,鏈式調用還能用得上。
removeAllListeners(type) { if (isNullOrUndefined(this._events)) return this; // if not provide type, remove all if (isNullOrUndefined(type)) this._events = Object.create(null); const events = this._events[type]; if (!isNullOrUndefined(events)) { // check if type is the last one if (Object.keys(this._events).length === 1) { this._events = Object.create(null); } else { delete this._events[type]; } } return this; }
removeAllListeners
指的是要刪除一個 type
對應的全部方法。參數 type
是可選的,若是未指定 type
,默認把全部的監聽事件刪除,直接 this._events = Object.create(null)
操做便可,跟初始化 EventEmitter
類同樣。
若是 events
既不是 null
且不是 undefined
說明有可刪除的 type
,先用 Object.keys(this._events).length === 1
判斷是否是最後一個 type
了,若是是,直接初始化 this._events = Object.create(null)
,不然 delete this._events[type]
直接刪除 type
屬性,一步到位。
最後返回 this
。
到目前爲止,全部的核心功能已經講完。
listeners(type) { if (isNullOrUndefined(this._events)) return []; const events = this._events[type]; // use `map` because we need to return a new array return isNullOrUndefined(events) ? [] : (typeof events === 'function' ? [events] : events.map(o => o)); } listenerCount(type) { if (isNullOrUndefined(this._events)) return 0; const events = this._events[type]; return isNullOrUndefined(events) ? 0 : (typeof events === 'function' ? 1 : events.length); } eventNames() { if (isNullOrUndefined(this._events)) return []; return Object.keys(this._events); }
listeners
返回的是 type
對應的全部方法。結果都是一個數組,若是沒有,返回空數組;若是隻有一個,把它的方法放到一個數組中返回;若是原本就是一個數組,map
返回。之因此用 map
返回而不是直接 return this._events[type]
是由於 map
返回一個新的數組,是深度複製,修改數組中的值不會影響到原數組。this._events[type]
則返回原數組的一個引用,是淺度複製,稍不當心改變值會影響到原數組。形成這個差別的底層緣由是數組是一個引用類型,淺度複製只是指針拷貝。這能夠單獨寫一篇文章,不展開了。
listenerCount
返回的是 type
對應的方法的個數,代碼一眼就明白,很少說。
eventNames
這個返回的是全部 type
組成的數組,沒有返回空數組,不然用 Object.keys(this._events)
直接返回。
最後的最後,export default EventEmitter
把 EventEmitter
導出。
我是先看了兩個庫才知道怎麼寫的,其實最好的學習方法是知道 EventEmitter
是幹什麼用的之後本身動手寫,寫完之後再和那些庫進行對比,找出差距,修正再修正。
但也不是說先看再寫沒有收穫,至少比只看不寫和看都沒看的有收穫不是。。。
水平有限,代碼錯漏或者文章講不清楚之處在所不免,歡迎你們批評指正。