最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • EventEmitter 的核心功能实现

    正文概述 掘金(Fstar)   2021-02-20   526

    EventEmitter 是频率较高的前端面试题。另外我个人在开发一款 SVG 编辑器,很多模块需要用到观察者模式来实现解耦。但 EventEmitter 是 Nodejs 环境下才能使用的库,所以不能直接用于浏览器环境的开发。所以我考虑自己实现一套逻辑,自己定制的话也容易根据实际情况的变动做修改。因此我决定了解一下 EventEmitter 的 API,并尝试自己实现一套逻辑。

    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)
    // 获取注册的事件名的数组形式
    emitter.eventNames()
    
    • 监听者函数的 this 会指向 EventEmitter 实例。当然你可以使用各种方法修改 this 的指向,如箭头函数或 bind 方法。
    • 每次添加监听器时,都会触发 newListener 事件,传入的参数为事件名(eventName)和监听器函数(listener)。
    • 同样,移除监听器时,会触发 removeListener 事件
    • emitter.prependListener():同 on,但会添加到监听器数组的开头。
    • ...

    API 很多,但我不打算实现了这么多,就只实现最常用的 on、emit、off。

    实现

    首先,我们知道不同的事件是有特定的 eventName(事件名)的,通过指定 eventName,我们才能绑定对应的多个监听器(函数),才能触发事件执行绑定的这些监听器。这时候,我们就涉及到数据结构与算法的存储问题了。因为结构和算法是相辅相成的,选择不同的数据结构,使用的算法就会不同。不同的数据结构与算法的优点的缺陷各不相同,比如空间复杂度上或时间复杂度上的效率不同。

    listener 函数的存储

    那么如何存储呢?常见的方法是使用哈希表,因为时间复杂度是 O(1),空间复杂度一般也不会太大。JavaScript 的对象本质上就是哈希表。所以我们的存储方式是:

    this.hashMap = {
      'event1': [listener1, listenr2],
      'event2': [],
    }
    

    一些可扩展的点:

    • 哈希表的一个问题是:无序。可以通过额外使用一个数组来记录添加 eventName 的记录顺序。这样的话,实现 emitter.eventNames() 可以拿到有序的事件数据。当然这样的需求比较少见,这里只是简单提一下。
    • 如果要实现 once(设置执行一次就不再执行的监听器函数),则需要对做函数标记,这时候可以考虑让数组元素的格式改为 { listener: Listener, once: boolean },在触发事件的时候,执行监听器函数时,将 once 值为 true 的监听器从数组中移除。
    • 可以改为链表实现存储,这样移除中间监听器时,时间复杂度可以变成 O(1)。另外数组删除元素的时间复杂度是 O(n)。但会引入实现上的复杂度,因为没有内置的链表实现,需要自己手动实现一个没有 BUG 的链表类。

    on() 的实现

    on() 的实现,其实就是将监听器函数绑定到指定事件对应的数组中。实现起来并不难,只要注意如果是第一次添加指定事件时,要先初始化一个空数组即可。on 最后返回了 this,是为了实现链式调用。

    class EventEmiter {
      on(eventName, listener) {
        if (!this.hashMap[eventName]) {
          this.hashMap[eventName] = []
        }
        this.hashMap[eventName].push(listener)
        return this
      }
    }
    

    off() 的实现

    off() 会根据传入的事件名,找到对应的监听器数组,从中移除指定监听器。同样为了实现链式调用返回了 this。

    class EventEmiter {
      off(eventName, listener): this {
        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() 的实现

    emit() 的实现很简单,找到事件对应的监听器,传入参数依次执行。如果事件没有绑定监听器,返回 false。否则,返回 true

    class EventEmiter {
      emit(eventName, ...args): boolean {
        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<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
      }
    }
    

    因为对象不支持 Symbol 作为索引,所以这里的实现做了类型的强转。未来,TypeScript 可能会允许对象索引为 Symbol,Enum 等,但目前不行。


    起源地下载网 » EventEmitter 的核心功能实现

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元