当前位置: 首页 > 科技观察

EventEmitter核心功能实现

时间:2023-03-15 21:18:08 科技观察

大家好,我是前端西瓜哥。EventEmitter是前端面试的一个常见问题。EventEmitter是一个只能在Nodejs环境下使用的库,不能直接用于浏览器环境的开发。于是考虑自己实现一套逻辑。如果我自定义的话,很容易根据实际情况修改。所以我决定看一下EventEmitter的API,尝试自己实现一套逻辑。Nodejs的EventEmitterAPI首先要了解需求,即EventEmitter的API用法。详细使用方法请参考官方文档。这里只简单介绍一些常用的API。const{EventEmitter,errorMonitor}=require('events');//创建事件触发实例constemitter=newEventEmitter()//on:注册监听函数。可以注册多个监听函数,//事件触发后,会依次同步执行,顺序就是绑定的顺序。//Aliasname:addListeneremitter.on('event',function(a,b)=>{console.log('eventemit!',a,b)})//once:注册一个只会被执行一次Functionemitter.once('event',function()=>{console.log('eventemitonlyonce!')})//emit:触发事件,可提供参数。//如果有对应的监听函数,则返回true,否则返回falsemitter.emit('event',3,4)//比较特别的是,如果没有注册error事件的监听器,//触发错误时,错误不会被捕获,会直接报错;//如果注册了,错误会被捕获.on(errorMonitor,err=>{console.log('errormonitor')});//移除指定监听器,别名为:removeListeneremitter.off(eventName,handler)//获取注册事件的数组形式nameemitter.eventNames()侦听器函数的this将指向EventEmitter实例。当然,你可以使用各种方法来修改this的指向,比如箭头函数或者bind方法。每添加一个监听器,都会触发newListener事件,传入的参数是事件名(eventName)和监听器函数(listener)。同样,当一个监听器被移除时,removeListener事件被触发。emitter.prependListener():与on相同,但会添加到监听器数组的开头。...API有很多,但我不打算实现那么多,只实现最常用的on、emit、off。首先,我们知道不同的事件有一个特定的eventName(事件名称)。通过指定eventName,我们可以绑定多个对应的监听器(函数),并触发事件执行绑定的监听器。这个时候我们就涉及到了数据结构和算法的存储。因为结构和算法是相辅相成的,选择不同的数据结构就会产生不同的算法。不同的数据结构和算法有不同的优缺点,比如空间复杂度或时间复杂度的效率不同。监听函数的存储那么如何存储呢?一种常用的方法是使用哈希表,因为时间复杂度是O(1),空间复杂度一般不会太大。JavaScript对象本质上是哈希表。所以我们的存储方式是:this.hashMap={'event1':[listener1,listenr2],'event2':[],}一些可扩展点:hash表的一个问题是:乱序。eventName的添加顺序可以另外使用一个数组来记录。在这种情况下,实施emitter.eventNames()以获取有序的事件数据。当然,这样的需求比较少见,这里只是简单提一下。如果要实现一次(一个监听函数,设置为执行一次,不会执行),需要对函数进行标记。这时候可以考虑将数组元素的格式改为{listener:Listener,once:boolean},当事件触发时,执行监听函数时,将once值为true的监听器从数组中移除。可以改成链表存储,这样当去掉中间的listener后,时间复杂度可以变成O(1)。另外,删除数组元素的时间复杂度是O(n)。但是在实现上会引入复杂度,因为没有内置链表实现,需要自己手动实现一个没有BUG的链表类。on()的实现on()的实现其实就是将监听函数绑定到指定事件对应的数组上。实现起来并不难,只是注意,如果是第一次添加指定事件,必须先初始化一个空数组。on最后返回这个来实现链式调用。classEventEmiter{on(eventName,listener){if(!this.hashMap[eventName]){this.hashMap[eventName]=[]}this.hashMap[eventName].push(listener)returnthis}}off()实现off()会根据传入的事件名称找到对应的监听器数组,并从中移除指定的监听器。这也是为了实现链式调用而返回的。classEventEmiter{off(eventName,listener){constlisteners=this.hashMap[eventName]if(listeners&&listeners.length>0){constindex=listeners.indexOf(listener)if(index>-1){listeners.splice(index,1)}}returnthis}}emit()implementationemit()的实现很简单,找到事件对应的监听器,依次执行传入的参数。如果事件没有附加侦听器,则返回false。否则,返回真。classEventEmiter{emit(eventName,...args){constlisteners=this.hashMap[eventName]if(!listeners||listeners.length===0)returnfalselisteners.forEach(listener=>{listener(...args)})returntrue}}完整实现虽然很突然,但是我这里给出的是TypeScript实现,只要去掉类型声明就是JavaScript实现。当然,下面的代码是简单的单元测试,大概没问题。源码地址:typeEventName=string|symboltypeListener=(...args:any[])=>voidclassEventEmiter{privatehashMap:{[eventName:string]:Array}={}on(eventName:EventName,listener:Listener):this{constname=eventNameasstringif(!this.hashMap[name]){this.hashMap[name]=[]}this.hashMap[name].push(listener)返回this}emit(eventName:EventName,...args:any[]):boolean{constlisteners=this.hashMap[eventNameasstring]if(!listeners||listeners.length===0)returnfalselisteners.forEach(listener=>{listener(...args)})returntrue}off(eventName:EventName,listener:Listener):this{constlisteners=this.hashMap[eventNameasstring]if(listeners&&listeners.length>0){constindex=listeners.indexOf(listener)如果(index>-1){listeners.splice(index,1)}}returnthis}}因为对象不支持Symbol作为索引,所以这里的实未来TypeScript可能允许对象索引为Symbol、Enum等,但目前不允许。