问题引申
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');