恋の歌的logo
Auto
归档 标签

使用mitt来作为Vue3中的事件总线(EventBus)

发表于2022-02-14 21:44
更新于2023-02-13 18:28
分类于编程
总字数1.1K
阅读时长 ≈4 分钟

前言 🔗

使用 mitt 来作为 Vue3 中的事件总线(EventBus

正文 🔗

Vue2 中,我们习惯使用 new Vue() 来创建一个 Vue 实例

只使用这个实例来调用 $on 或者 $off 来添加或者删除事件回调,使用 $emit 来调用改事件的所有回调

这样就可以在不同组件之间进行数据传递

js
// emitter.js
import Vue from "vue";

export const emitter = new Vue();
js
// main.js
import { emitter } from "./emitter.js";

Vue.prototype.$emitter = emitter;

使用 emitter

js
// component-1
export default {
  methods: {
    send() {
      this.$emitter.$emit("eventName", "data");
    },
  },
};
js
// component-2
export default {
  methods: {
    callback(data) {
      // 打印 'data'
      console.log(data);
    },
  },
  mounted() {
    this.$emitter.$on("eventName", this.callback);
  },
  beforeDestroy() {
    this.$emitter.$off("eventName", this.callback);
  },
};

Vue3 中,官方已经不推荐使用 new Vue() 来构造事件总线了

而推荐使用 mitt 或者 tiny-emitter 库来进行替代

事件总线 - 事件 API | Vue.js

这两个类库的实现都是差不多的

mitt 🔗

mitt - Github

使用 TS 编写,有完整的类型推断

支持 * 作为事件名

* 作为事件名即任何 emit 都会触发这些事件回调

源码如下(删除部分 TS 类型代码)

typescript
export default function mitt<Events extends Record<EventType, unknown>>(
  all?: EventHandlerMap<Events>
): Emitter<Events> {
  all = all || new Map();

  return {
    // 存放 eventName -> handler[]
    all,

    on<Key extends keyof Events>(type: Key, handler: GenericEventHandler) {
      // 从 all 这个 map 中拿到对应名字的回调列表
      const handlers: Array<GenericEventHandler> | undefined = all!.get(type);

      // 加入,特殊处理第一次加入的情况
      if (handlers) {
        handlers.push(handler);
      } else {
        all!.set(type, [handler] as EventHandlerList<Events[keyof Events]>);
      }
    },

    off<Key extends keyof Events>(type: Key, handler?: GenericEventHandler) {
      const handlers: Array<GenericEventHandler> | undefined = all!.get(type);
      if (handlers) {
        // 找到 handler 然后从数组中删除
        if (handler) {
          handlers.splice(handlers.indexOf(handler) >>> 0, 1);
        } else {
          // 不传 handler 则删除该事件名的全部回调
          all!.set(type, []);
        }
      }
    },

    emit<Key extends keyof Events>(type: Key, evt?: Events[Key]) {
      let handlers = all!.get(type);
      if (handlers) {
        (handlers as EventHandlerList<Events[keyof Events]>)
          .slice()
          .map((handler) => {
            // 执行
            handler(evt!);
          });
      }

      // 处理 * 事件名的情况
      handlers = all!.get("*");
      if (handlers) {
        (handlers as WildCardEventHandlerList<Events>)
          .slice()
          .map((handler) => {
            // 作为 * 的回调,会在第一个参数传入触发它的事件名,和具名事件的回调存在一定区别
            handler(type, evt!);
          });
      }
    },
  };
}

tiny-emitter 🔗

tiny-emitter - Github

index.d.ts 文件,不过源码使用 js 编写

不支持 * 作为事件名,不过封装了 once 方法

源码如下:

js
// 构造函数
function E() {}

// 覆盖原型对象,挂载公共方法
E.prototype = {
  on: function (name, callback, ctx) {
    // 使用实例上的 `e` 属性来保存 eventName -> handler[]
    var e = this.e || (this.e = {});

    (e[name] || (e[name] = [])).push({
      fn: callback,
      ctx: ctx,
    });

    // 返回 this , 这样可以链式调用
    return this;
  },

  once: function (name, callback, ctx) {
    var self = this;
    // 二次封装 callback
    // 使得被 emit 之后顺便 off 掉这个回调,这样就只执行一次
    function listener() {
      self.off(name, listener);
      callback.apply(ctx, arguments);
    }

    // 把原 callback 挂载到 _  属性上,在 off 的时候可以正确地判断
    listener._ = callback;
    return this.on(name, listener, ctx);
  },

  emit: function (name) {
    // 提取需要传递的数据
    var data = [].slice.call(arguments, 1);
    // 获取该事件名的回调的列表
    var evtArr = ((this.e || (this.e = {}))[name] || []).slice();
    var i = 0;
    var len = evtArr.length;

    for (i; i < len; i++) {
      // 调用
      evtArr[i].fn.apply(evtArr[i].ctx, data);
    }

    return this;
  },

  off: function (name, callback) {
    var e = this.e || (this.e = {});
    var evts = e[name];
    var liveEvents = [];

    if (evts && callback) {
      for (var i = 0, len = evts.length; i < len; i++) {
        // 由于 once 二次封装了 callback
        // 这样如果在事件未被 emit 之前调用 off 的话,虽然传给 off 的 callback 和 传给 once 的 callback 一样
        // 但是无法 off 掉, 所以要加另一个判断 evts[i].fn._ !== callback 来判断是否相同
        // 因为 once 把原 callback 挂载在了二次封装 callback 的 `_` 属性上
        if (evts[i].fn !== callback && evts[i].fn._ !== callback)
          liveEvents.push(evts[i]);
      }
    }

    // 如果 off 掉某个 callback 之后剩下的 callback 列表为空, 那么删除该 eventName 对应的 callback 列表
    // 否则则直接替换 callback 列表
    liveEvents.length ? (e[name] = liveEvents) : delete e[name];

    return this;
  },
};

module.exports = E;
module.exports.TinyEmitter = E;

后记 🔗

虽然事件总线简单易用,但是当代码复杂度上升到一定程度之后,过多的事件监听会让数据流变得晦涩难懂

官方并不鼓励使用全局的事件总线来进行组件间的通信

我们可以通过其他的方法来实现相同的效果

比如提到的

  • propemit (父子组件通信)
  • provideinject (父传后代)
  • exposeref (子传父)
  • 全局状态管理 Vuex 或者 Pinia (提取全局状态)
  • v-slot 暴露变量(子传父)
#JavaScript
#Vue
#EventBus
#mitt
哦呐该,如果没有评论的话,瓦达西...