大家好,心功现我是心功现前端西瓜哥。 EventEmitter 是心功现频率较高的前端面试题。 EventEmitter 是心功现 Nodejs 环境下才能使用的库,所以不能直接用于浏览器环境的心功现开发。所以我考虑自己实现一套逻辑,心功现自己定制的心功现话也容易根据实际情况的变动做修改。 因此我决定了解一下 EventEmitter 的心功现 API,并尝试自己实现一套逻辑。心功现 首先当然是要了解需求,即 EventEmitter 的心功现 API 使用。详细使用方式请查阅 官方文档,心功现我这里只简单叙述一些常用的心功现 API。 const { EventEmitter,心功现 errorMonitor } = require(events); // 创建事件触发器实例 const emitter = new EventEmitter() // on:注册监听者函数。可以注册多个监听函数,心功现 // 触发事件后,会依次同步执行,顺序为绑定时的顺序。 // 别名为:addListener emitter.on(event, function(a, b) => { console.log(event emit!, a, b) }) // once:注册一个只会被执行一次的函数 emitter.once(event, function() => { console.log(event emit only once!) }) // emit:触发事件,站群服务器可提供参数。 // 如果有对应监听器函数,会返回 true,否则返回 false emitter.emit(event, 3, 4) // 比较特别的是,如果没有注册 error 事件的监听者, // 触发 error 时,错误不会被捕获而直接报错; // 若注册,则错误会被捕获 emitter.emit(error, new Error(whoops!)) // event.errorMonitor 是一个 Symbol // 能够在触发 error 事件时,先执行被绑定的监视器函数 emitter.on(errorMonitor, err => { console.log(error monitor) }); // 移除指定监听器,别名为:removeListener emitter.off(eventName, handler) // 获取注册的事件名的数组形式 API 很多,但我不打算实现了这么多,服务器租用就只实现最常用的 on、emit、off。 首先,我们知道不同的事件是有特定的 eventName(事件名)的,通过指定 eventName,我们才能绑定对应的多个监听器(函数),才能触发事件执行绑定的这些监听器。 这时候,我们就涉及到数据结构与算法的存储问题了。因为结构和算法是相辅相成的,选择不同的数据结构,使用的算法就会不同。 不同的数据结构与算法的优点的缺陷各不相同,比如空间复杂度上或时间复杂度上的效率不同。 那么如何存储呢?常见的亿华云方法是使用哈希表,因为时间复杂度是 O(1),空间复杂度一般也不会太大。JavaScript 的对象本质上就是哈希表。所以我们的存储方式是: this.hashMap = { event1: [listener1, listenr2], event2: [], 一些可扩展的点: on() 的实现,其实就是将监听器函数绑定到指定事件对应的数组中。实现起来并不难,只要注意如果是第一次添加指定事件时,要先初始化一个空数组即可。on 最后返回了 this,是为了实现链式调用。 class EventEmiter { on(eventName, listener) { if (!this.hashMap[eventName]) { this.hashMap[eventName] = [] } this.hashMap[eventName].push(listener) return this } off() 会根据传入的事件名,找到对应的监听器数组,从中移除指定监听器。同样为了实现链式调用返回了 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() 的实现很简单,找到事件对应的监听器,传入参数依次执行。如果事件没有绑定监听器,返回 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 } 虽然很突然,我这里给出的是 TypeScript 实现,只要将类型声明去掉就是 JavaScript 实现了。当然下面代码是做了简单的单元测试的,大概是没问题的。 源码地址: type EventName = string | symbol type Listener = (...args: any[]) => void class EventEmiter { private hashMap: { [eventName: string]: Array 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 } 因为对象不支持 Symbol 作为索引,所以这里的实现做了类型的强转。未来,TypeScript 可能会允许对象索引为 Symbol,Enum 等,但目前不行。