跨组件通讯之手动实现 事件总线,模仿mitt

问题引申

vue中经常需要进行组件之间通讯,通常我们使用父组件props和子组件emit的形式进行父子组件通讯,如果要进行跨越N代组件通讯,则经常使用祖先组件provide与后代组件inject进行通讯,如果要进行则有时我们会借助状态管理工具vuex或者pinia进行响应式管理。

同代组件之间用状态管理工具的困扰

如果同代之间使用状态管理工具进行响应式管理,我们需要预先定义一个store,在相应的store中定义好state和(Action / Mutation),这样子笔者认为是一件十分头疼的问题,因为我们有时候只需要使用一次该变量,但是却要定义一个store等等,如此繁杂。

基于发布订阅模式下的一对多的事件总线

订阅者通过on方法进行事件订阅,发布者通过emit方法进行事件发布通知相应订阅的组件进行相应的处理。

代码

export type EventFunction = (...args: any[]) => any;
export type EventNoneParamFunction = () => any;
export interface EventType {
  once: boolean,
  events: Array<EventFunction | EventNoneParamFunction>
}
export interface EventMaps {
  [index: string]: EventType
}

/**
 * 事件总线
 */
export default class Bus {
  private events: EventMaps = {}
  constructor() {

  }

  /**
   * 查询事件是否存在事件中心中
   * @param eventName 监听的事件名称
   */
  private hasEvent(eventName: string): EventType | undefined {
    return this.events[eventName];
  }

  /**
   * 判断某回调函数是否存在监听事件的回调函数数组中
   * @param nowEvents
   * @param fn
   */
  private eventHasFn(nowEvents: Array<EventFunction>, fn: EventFunction) {
    return nowEvents.findIndex(ev => ev === fn);
  }

  /**
   * 获取当前所有监听事件名
   */
  getEvents() {
    return Object.keys(this.events);
  }

  /**
   * 获取监听事件的所有函数
   * @param eventName
   */
  getEventFns(eventName: string) {
    // 查询是否存在相应监听的事件名称
    const nowEvents = this.hasEvent(eventName);
    return nowEvents? nowEvents.events : null;
  }

  /**
   * 注册相应事件的回调的函数
   * @param eventName 监听的事件名称
   * @param fn 回调函数
   * @param once 是否只触发一次
   */
  on(eventName: string, fn: EventFunction | EventNoneParamFunction, once: boolean = false) {
    if(!eventName) return console.error("第一个参数错误");
    // 查询是否存在相应监听的事件名称
    const nowEvents = this.hasEvent(eventName);
    // 判断第二个参数是否为函数
    if(typeof fn !== 'function') return console.error("第二个参数传递错误");
    if(!nowEvents) {
      // 不存在相应监听的事件名称直接加入到map中,并且将回调函数包裹成数组以便调用
      this.events[eventName] = {
        once,
        events: [fn]
      };
    } else {
      // 存在时需要判断是否在相应监听事件中是否有相同的回调函数,因Function的传递为址传递所以直接对比地址值是否相同即可
      const fnIndex = this.eventHasFn(nowEvents.events, fn);
      // 当不存在相应的回调函数时则加入到相应监听事件的回调数组中
      if(fnIndex === -1) {
        nowEvents.events.push(fn);
      }

      nowEvents.once = once;
    }
  }

  /**
   * 触发监听事件的回调函数
   * @param eventName 监听的事件名称
   * @param args 回调事件传递的参数
   */
  emit(eventName: string, ...args: any[]) {
    // 查询是否存在相应监听的事件名称
    const nowEvents = this.hasEvent(eventName);
    if(!nowEvents) return console.error("不存在相应的监听事件");
    else {
      let backDataCollect: any[] = [];
      if(nowEvents.events.length) {
        backDataCollect = nowEvents.events.map(ev => {
          if(typeof ev === 'function') return ev(...args);
          else return null;
        })
      }

      if(nowEvents.once) this.off(eventName);
      return backDataCollect;
    }
  }

  /**
   * 卸载相应的监听事件
   * @param eventName 监听的事件名称
   */
  off(eventName: string) {
    // 查询是否存在相应监听的事件名称
    const nowEvents = this.hasEvent(eventName);
    if(!nowEvents) return console.error(`未监听${eventName}事件`);
    else delete this.events[eventName];
  }

  /**
   * 监听相应事件,但只触发一次回调函数
   * @param eventName
   * @param fn
   */
  once(eventName: string, fn: EventFunction | EventNoneParamFunction) {
    this.on(eventName, fn, true);
  }

  /**
   * 强制重装监听函数,忽略之前监听函数中的所有回调函数
   * @param eventName
   * @param fn
   * @param once
   */
  forceOn(eventName: string, fn: EventFunction | EventNoneParamFunction, once: boolean = false) {
    // 查询是否存在相应监听的事件名称
    const nowEvents = this.hasEvent(eventName);
    if(nowEvents) nowEvents.events = [];
    this.on(eventName, fn, once);
  }
}

事件总线解决方案

我们可以通过原型链将事件中心挂载到Vue原型 / 全局变量上,在组件之间注册监听事件。当组件接受到通知时则进行相应的处理,在被提醒的组件中注册相应的监听事件(on),其他组件通知时则调用emit进行相应事件的触发。

使用方法

挂载Vue原型中

// 创建事件中心实例
const eventBus = new Bus();
// 挂载Vue原型
const app = createApp(App);
app.config.globalProperties.$Bus = eventBus;

A组件中注册监听事件

import {getCurrentInstance, ref} from "vue";
const {proxy} = getCurrentInstance() as any;

const test = ref('')

proxy.$Bus.on('test', function (name: string) {
  console.log("new Test1: " + name)
})
// 组件销毁时,销毁相应事件监听
onUnmounted(() => {
  proxy.$Bus.off('test')
})
// 如果不想进行组件销毁时销毁相应的监听事件时,又害怕监听事件的回调函数中取的是name的旧值,则可以这样使用,即忽略之前注册的监听事件的所有回调函数,重新注册一个新的回调数组
proxy.$Bus.forceOn('test', function (name: string) {
  console.log("new Test2: " + name);
  test.value = name;
})

B组件中触发监听事件

import {getCurrentInstance} from "vue";
const {proxy} = getCurrentInstance() as any;
proxy.$Bus.emit('test', 'xiao ming');
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值