实现事件总线结合Vue应用
手写一个事件总线(EventBus)并在Vue应用中实现组件间通信
问题
在Vue应用中,组件间通信是常见需求。对于非父子组件或跨层级组件通信,事件总线(EventBus)是一种轻量级的解决方案。本题要求实现一个事件总线系统,支持事件的订阅、发布和取消订阅,并展示如何在Vue应用中使用。
解答
// EventBus.js - 事件总线实现
class EventBus {
constructor() {
// 存储事件及其对应的回调函数
this.events = {};
}
/**
* 订阅事件
* @param {string} eventName - 事件名称
* @param {Function} callback - 回调函数
* @param {Object} context - 回调函数的上下文(this指向)
*/
on(eventName, callback, context = null) {
if (!eventName || typeof callback !== 'function') {
console.warn('事件名称和回调函数不能为空');
return;
}
// 如果事件不存在,创建一个新数组
if (!this.events[eventName]) {
this.events[eventName] = [];
}
// 添加回调函数和上下文
this.events[eventName].push({
callback,
context
});
}
/**
* 订阅一次性事件(触发后自动取消订阅)
* @param {string} eventName - 事件名称
* @param {Function} callback - 回调函数
* @param {Object} context - 回调函数的上下文
*/
once(eventName, callback, context = null) {
// 包装回调函数,执行后自动取消订阅
const wrappedCallback = (...args) => {
callback.apply(context, args);
this.off(eventName, wrappedCallback);
};
this.on(eventName, wrappedCallback, context);
}
/**
* 触发事件
* @param {string} eventName - 事件名称
* @param {...any} args - 传递给回调函数的参数
*/
emit(eventName, ...args) {
if (!this.events[eventName]) {
return;
}
// 执行所有订阅该事件的回调函数
this.events[eventName].forEach(({ callback, context }) => {
callback.apply(context, args);
});
}
/**
* 取消订阅事件
* @param {string} eventName - 事件名称
* @param {Function} callback - 要取消的回调函数(可选)
*/
off(eventName, callback = null) {
if (!this.events[eventName]) {
return;
}
// 如果没有指定回调函数,删除该事件的所有订阅
if (!callback) {
delete this.events[eventName];
return;
}
// 删除指定的回调函数
this.events[eventName] = this.events[eventName].filter(
item => item.callback !== callback
);
// 如果该事件没有订阅者了,删除该事件
if (this.events[eventName].length === 0) {
delete this.events[eventName];
}
}
/**
* 清空所有事件订阅
*/
clear() {
this.events = {};
}
}
// 导出单例
export default new EventBus();
// Vue 2.x 插件形式
// eventBusPlugin.js
import EventBus from './EventBus';
export default {
install(Vue) {
// 将事件总线挂载到 Vue 原型上
Vue.prototype.$bus = EventBus;
}
};
// Vue 3.x 插件形式
// eventBusPlugin.js (Vue 3)
import EventBus from './EventBus';
export default {
install(app) {
// 将事件总线挂载到全局属性上
app.config.globalProperties.$bus = EventBus;
// 也可以通过 provide 提供
app.provide('$bus', EventBus);
}
};
使用示例
// main.js - Vue 2.x
import Vue from 'vue';
import App from './App.vue';
import EventBusPlugin from './eventBusPlugin';
Vue.use(EventBusPlugin);
new Vue({
render: h => h(App)
}).$mount('#app');
<!-- ComponentA.vue - 发送消息的组件 -->
<template>
<div class="component-a">
<h3>组件 A</h3>
<input v-model="message" placeholder="输入消息" />
<button @click="sendMessage">发送消息</button>
</div>
</template>
<script>
export default {
name: 'ComponentA',
data() {
return {
message: ''
};
},
methods: {
sendMessage() {
// 触发事件,传递数据
this.$bus.emit('message-sent', {
content: this.message,
timestamp: Date.now()
});
this.message = '';
}
}
};
</script>
<!-- ComponentB.vue - 接收消息的组件 -->
<template>
<div class="component-b">
<h3>组件 B</h3>
<div v-if="receivedMessage">
<p>收到消息: {{ receivedMessage.content }}</p>
<p>时间: {{ formatTime(receivedMessage.timestamp) }}</p>
</div>
</div>
</template>
<script>
export default {
name: 'ComponentB',
data() {
return {
receivedMessage: null
};
},
created() {
// 订阅事件
this.$bus.on('message-sent', this.handleMessage, this);
},
beforeDestroy() {
// 组件销毁前取消订阅,防止内存泄漏
this.$bus.off('message-sent', this.handleMessage);
},
methods: {
handleMessage(data) {
this.receivedMessage = data;
},
formatTime(timestamp) {
return new Date(timestamp).toLocaleTimeString();
}
}
};
</script>
// 在 Composition API 中使用 (Vue 3)
import { onMounted, onUnmounted, getCurrentInstance } from 'vue';
export default {
setup() {
const instance = getCurrentInstance();
const $bus = instance.appContext.config.globalProperties.$bus;
const handleMessage = (data) => {
console.log('收到消息:', data);
};
onMounted(() => {
$bus.on('message-sent', handleMessage);
});
onUnmounted(() => {
$bus.off('message-sent', handleMessage);
});
return {};
}
};
关键点
-
发布-订阅模式:事件总线基于发布-订阅模式,实现组件间的解耦通信
-
事件存储结构:使用对象存储事件名和回调函数数组的映射关系,支持一个事件多个订阅者
-
上下文绑定:保存回调函数的上下文(context),确保回调函数中的
this指向正确 -
一次性订阅:
once方法通过包装回调函数,在执行后自动取消订阅 -
内存泄漏防范:组件销毁时必须取消事件订阅(在
beforeDestroy或onUnmounted中调用off) -
灵活的取消订阅:支持取消指定回调或取消某个事件的所有订阅
-
Vue 集成方式:通过插件形式挂载到 Vue 原型或全局属性上,方便在组件中使用
-
参数传递:使用剩余参数(
...args)支持传递任意数量和类型的参数 -
错误处理:添加基本的参数校验,提高代码健壮性
-
单例模式:导出事件总线的单例实例,确保全局使用同一个实例
目录