EventEmitter 的核心功能實(shí)現(xiàn)

大家好,我是前端西瓜哥。
EventEmitter 是頻率較高的前端面試題。
EventEmitter 是 Nodejs 環(huán)境下才能使用的庫,所以不能直接用于瀏覽器環(huán)境的開發(fā)。所以我考慮自己實(shí)現(xiàn)一套邏輯,自己定制的話也容易根據(jù)實(shí)際情況的變動(dòng)做修改。
因此我決定了解一下 EventEmitter 的 API,并嘗試自己實(shí)現(xiàn)一套邏輯。
Nodejs 的 EventEmitter API
首先當(dāng)然是要了解需求,即 EventEmitter 的 API 使用。詳細(xì)使用方式請查閱 官方文檔,我這里只簡單敘述一些常用的 API。
const { EventEmitter, errorMonitor } = require('events');
// 創(chuàng)建事件觸發(fā)器實(shí)例
const emitter = new EventEmitter()
// on:注冊監(jiān)聽者函數(shù)。可以注冊多個(gè)監(jiān)聽函數(shù),
// 觸發(fā)事件后,會(huì)依次同步執(zhí)行,順序?yàn)榻壎〞r(shí)的順序。
// 別名為:addListener
emitter.on('event', function(a, b) => {
console.log('event emit!', a, b)
})
// once:注冊一個(gè)只會(huì)被執(zhí)行一次的函數(shù)
emitter.once('event', function() => {
console.log('event emit only once!')
})
// emit:觸發(fā)事件,可提供參數(shù)。
// 如果有對應(yīng)監(jiān)聽器函數(shù),會(huì)返回 true,否則返回 false
emitter.emit('event', 3, 4)
// 比較特別的是,如果沒有注冊 error 事件的監(jiān)聽者,
// 觸發(fā) error 時(shí),錯(cuò)誤不會(huì)被捕獲而直接報(bào)錯(cuò);
// 若注冊,則錯(cuò)誤會(huì)被捕獲
emitter.emit('error', new Error('whoops!'))
// event.errorMonitor 是一個(gè) Symbol
// 能夠在觸發(fā) error 事件時(shí),先執(zhí)行被綁定的監(jiān)視器函數(shù)
emitter.on(errorMonitor, err => {
console.log('error monitor')
});
// 移除指定監(jiān)聽器,別名為:removeListener
emitter.off(eventName, handler)
// 獲取注冊的事件名的數(shù)組形式
emitter.eventNames()
- 監(jiān)聽者函數(shù)的 this 會(huì)指向 EventEmitter 實(shí)例。當(dāng)然你可以使用各種方法修改 this 的指向,如箭頭函數(shù)或 bind 方法。
- 每次添加監(jiān)聽器時(shí),都會(huì)觸發(fā) newListener 事件,傳入的參數(shù)為事件名(eventName)和監(jiān)聽器函數(shù)(listener)。
- 同樣,移除監(jiān)聽器時(shí),會(huì)觸發(fā) removeListener 事件。
- emitter.prependListener():同 on,但會(huì)添加到監(jiān)聽器數(shù)組的開頭。
- ...
API 很多,但我不打算實(shí)現(xiàn)了這么多,就只實(shí)現(xiàn)最常用的 on、emit、off。
實(shí)現(xiàn)
首先,我們知道不同的事件是有特定的 eventName(事件名)的,通過指定 eventName,我們才能綁定對應(yīng)的多個(gè)監(jiān)聽器(函數(shù)),才能觸發(fā)事件執(zhí)行綁定的這些監(jiān)聽器。
這時(shí)候,我們就涉及到數(shù)據(jù)結(jié)構(gòu)與算法的存儲問題了。因?yàn)榻Y(jié)構(gòu)和算法是相輔相成的,選擇不同的數(shù)據(jù)結(jié)構(gòu),使用的算法就會(huì)不同。
不同的數(shù)據(jù)結(jié)構(gòu)與算法的優(yōu)點(diǎn)的缺陷各不相同,比如空間復(fù)雜度上或時(shí)間復(fù)雜度上的效率不同。
listener 函數(shù)的存儲
那么如何存儲呢?常見的方法是使用哈希表,因?yàn)闀r(shí)間復(fù)雜度是 O(1),空間復(fù)雜度一般也不會(huì)太大。JavaScript 的對象本質(zhì)上就是哈希表。所以我們的存儲方式是:
this.hashMap = {
'event1': [listener1, listenr2],
'event2': [],
}
一些可擴(kuò)展的點(diǎn):
- 哈希表的一個(gè)問題是:無序??梢酝ㄟ^額外使用一個(gè)數(shù)組來記錄添加 eventName 的記錄順序。這樣的話,實(shí)現(xiàn) emitter.eventNames() 可以拿到有序的事件數(shù)據(jù)。當(dāng)然這樣的需求比較少見,這里只是簡單提一下。
- 如果要實(shí)現(xiàn) once(設(shè)置執(zhí)行一次就不再執(zhí)行的監(jiān)聽器函數(shù)),則需要對函數(shù)標(biāo)記,這時(shí)候可以考慮讓數(shù)組元素的格式改為 { listener: Listener, once: boolean },在觸發(fā)事件的時(shí)候,執(zhí)行監(jiān)聽器函數(shù)時(shí),將 once 值為 true 的監(jiān)聽器從數(shù)組中移除。
- 可以改為鏈表實(shí)現(xiàn)存儲,這樣移除中間監(jiān)聽器時(shí),時(shí)間復(fù)雜度可以變成 O(1)。另外數(shù)組刪除元素的時(shí)間復(fù)雜度是 O(n)。但會(huì)引入實(shí)現(xiàn)上的復(fù)雜度,因?yàn)闆]有內(nèi)置的鏈表實(shí)現(xiàn),需要自己手動(dòng)實(shí)現(xiàn)一個(gè)沒有 BUG 的鏈表類。
on() 的實(shí)現(xiàn)
on() 的實(shí)現(xiàn),其實(shí)就是將監(jiān)聽器函數(shù)綁定到指定事件對應(yīng)的數(shù)組中。實(shí)現(xiàn)起來并不難,只要注意如果是第一次添加指定事件時(shí),要先初始化一個(gè)空數(shù)組即可。on 最后返回了 this,是為了實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用。
class EventEmiter {
on(eventName, listener) {
if (!this.hashMap[eventName]) {
this.hashMap[eventName] = []
}
this.hashMap[eventName].push(listener)
return this
}
}
off() 的實(shí)現(xiàn)
off() 會(huì)根據(jù)傳入的事件名,找到對應(yīng)的監(jiān)聽器數(shù)組,從中移除指定監(jiān)聽器。同樣為了實(shí)現(xiàn)鏈?zhǔn)秸{(diào)用返回了 this。
class EventEmiter {
off(eventName, listener) {
const listeners = this.hashMap[eventName]
if (listeners && listeners.length > 0) {
const index = listeners.indexOf(listener)
if (index > -1) {
listeners.splice(index, 1)
}
}
return this
}
}
emit() 的實(shí)現(xiàn)
emit() 的實(shí)現(xiàn)很簡單,找到事件對應(yīng)的監(jiān)聽器,傳入?yún)?shù)依次執(zhí)行。如果事件沒有綁定監(jiān)聽器,返回 false。否則,返回 true。
class EventEmiter {
emit(eventName, ...args) {
const listeners = this.hashMap[eventName]
if (!listeners || listeners.length === 0) return false
listeners.forEach(listener => {
listener(...args)
})
return true
}
}
完整實(shí)現(xiàn)
雖然很突然,我這里給出的是 TypeScript 實(shí)現(xiàn),只要將類型聲明去掉就是 JavaScript 實(shí)現(xiàn)了。當(dāng)然下面代碼是做了簡單的單元測試的,大概是沒問題的。
源碼地址:
type EventName = string | symbol
type Listener = (...args: any[]) => void
class EventEmiter {
private hashMap: { [eventName: string]: Array<Listener> } = {}
on(eventName: EventName, listener: Listener): this {
const name = eventName as string
if (!this.hashMap[name]) {
this.hashMap[name] = []
}
this.hashMap[name].push(listener)
return this
}
emit(eventName: EventName, ...args: any[]): boolean {
const listeners = this.hashMap[eventName as string]
if (!listeners || listeners.length === 0) return false
listeners.forEach(listener => {
listener(...args)
})
return true
}
off(eventName: EventName, listener: Listener): this {
const listeners = this.hashMap[eventName as string]
if (listeners && listeners.length > 0) {
const index = listeners.indexOf(listener)
if (index > -1) {
listeners.splice(index, 1)
}
}
return this
}
}
因?yàn)閷ο蟛恢С?Symbol 作為索引,所以這里的實(shí)現(xiàn)做了類型的強(qiáng)轉(zhuǎn)。未來,TypeScript 可能會(huì)允許對象索引為 Symbol,Enum 等,但目前不行。



























