作为前端必须了解的事件分发机制

258 阅读3分钟

事件传递机制

一、从Vue的事件传递说起

组件间的信息传递是Vue框架中非常核心的部分,大多数情况下可以Prop/事件进行父子传递, Provide/inject允许一个Prop/事件内容传递或者组件与组件间的跨级传递, 或者使用笨重的vuex/pinia进行状态管理

但是Vue2.x中是存在有中央事件总线的, 即采用公共的Vue实例进行传递数据, emit/on发射和监听自定义事件, $off销毁事件, Vue3.x中因为创建实例使用creatApp({})的方法没办法和之前new Vue({})一样在实例上挂载, 所以官方建议mitt或者tiny-emitter三方库

优点: 三方库体积小, 传递方便

缺点: 状态无法记录, 不方便管理

二、从mitt入门事件传递

mitt库核心代码如下:

module.exports = function _mitt (n) {
  return {
    /**
     * 实例一个Map结构的 n 管理订阅者
     */
    all: (n = n || new Map()),
    /**
     * @param e代表事件名
     * @param t代表传入的fn回调
     */
    on: function (e, t) {
      let i = n.get(e);
      (i && i.push(t)) || n.set(e, [t])
    },
    /**
     * @param e为string或Symbol的事件名 也可以为 *
     * @param t为传递的参数
     */
    emit: function (e, t) {
      (n.get(e) || []).slice().map(item=>item(t)),
        (n.get("*") || []).slice().map(item=>item(e,t));
    },
    /**
     * @param e为事件名
     * @param t为需要对比的回调函数
     */
    off: function (e, t) {
      let i = n.get(e);
      let idx = i.indexOf(t);
      if(idx===0){
        i.splice(idx, 1)
      }else{
        (idx !== -1) ? (i && i.splice(idx, 1)) : ''
      }
    }
  }
}

不得不说, 真的十分精简!

但是提供的api还是不太齐全, 继续往下看

三、深入事件传递

在很多情况下,事件传递还会提供once/hasListener...这样的api以满足更复杂的业务场景

事件传递实现的流程:

事实上, 从mitt的代码可以看出事件传递时实际上是先订阅再发布

  1. 全局创建存储事件的集合, 事件名为key, 对应的值为value
  2. 首先需要订阅或者监听emit发射的某个事件, 参数为事件名和callback
  3. emit发射事件, 参数为事件名和被传递的数据data
  4. 经过3的 callback.call(null, data)完成数据接收

弄清楚事件传递流程之后, 接下来就想办法实现一些其他的比较重要的api

四、实现一个完整的事件分发机制

参考nodejsevents模块实现一些常用的事件分发api

export default class EventEmitter{
    //1.初始化events对象
    constructor(){
        this._events = new Map()
    }
    private _events
    //2.创建一些类型检测的工具函数
    private function _toString(){
        return Object.prototype.toString
    }
    private function _isType(obj){
        return this._toString.call(obj).slice(8,-1).toLowerCase()
    }
    private function _isArray(obj){
        return Array.isArray(obj) || this._isType(obj) === 'array'
    }
    private function _isNullOrUndefined(obj){
        return obj === null || obj === undefined
    }
    private function _addListener(type,fn,ctx,once?){
        //2.1 内部实现订阅
        if(typeof fn !== 'function'){
            throw new Error('fn must be a function!')
        }
        fn.context = ctx
        fn.once = !!once
        const events = this._events.get(type)
        //2.2 根据event类型不同做不同处理
        if(this._isNullOrUndefined(events)){
            this._events.set(type,fn)
            return true
        }else if(typeof events === 'function'){
            const activeListenFns = [events]
            activeListenFns.push(fn)
            this._events.set(type,activeListenFns)
            return true
        }else if(this._isArray(events)){
            const activeListenFns = events.push(fn)
            this._events.set(type,activeListenFns)
            return true
        }else{
            return false
        }
    }
    // 3.生成暴露出去的API函数
    addListener(type,fn,context){
        //3.1 收集订阅
        return this._addListener(type,fn,context)
    }
    once(type,fn,context){
        //3.2 只订阅本次 下次取消订阅
        return this._addListener(type,fn,context,true)
    }
    emit(type,...rest){
        //3.3 发布事件并传递数据
        if(this._isNullOrUndefined(type)){
             throw new Error('emit must receive at least one argument!')
        }
        const events = this._events.get(type)
        if(this._isNullOrUndefined(events)) return false
        if(typeof events === 'function'){
            events.call(events.context || null, rest)
            events.once? this.removeListener(type,event) : ''
        }else if(this._isArray(events)){
            events.map(cb=>{
                cb.call(cb.context || null, rest)
                cb.once? this.removeListener(type,cb) : ''
            })
        }
        return true
    }
    removeListener(type,fn){
        //3.4 移除对某个事件类型的某个监听
        if(this._isNullOrUndefined(this._events) || this._isNullOrUndefined(type)) return false
        if(typeof fn !== 'function'){
            throw new Error('fn must be a function')
        }
        const events = this._events.get(type)
        if(typeof events === 'function'){
            events === fn && this._events.remove(type)
        }else{
            const needToRemoveFnIdx = events.findIndex(e=> e===fn)
            needToRemoveFnIdx === -1? return false : events.splice(needToRemoveFnIdx,1)
        }
        return true
    }
    removeAllListeners(type){
        //3.4 移除对某个事件类型的全部监听
        if(this._isNullOrUndefined(this._events)) return false
        if(this._isNullOrUndefined(type)){
            this._events = new Map()
            return false
        }
        const events = this._events.get(type)
        if(!this._isNullOrUndefined(events)){
            this._events.remove(type)
        }
        return true
    }
}

以上就实现了一个具有once/removeListener/removeAllListeners的事件分发类, 当然如果还有其他需求可以继续添加功能( *比如获取某个事件类型的全部监听/监听总数... *), 这里只是把实现的流程和思想分享一下, 想要学习更多的话推荐参考nodejs中的events模块和eventmitter3库, 尤其这个第三方库现在每周都有200W+的下载量, 十分推荐手敲一遍