事件总线是一个用于组件间通信的模式,尤其适用于非父子组件或跨层级组件之间的通信。Vue 3 官方推荐使用 mitt 或 tiny-emitter 等第三方库替代 Vue 2 中的 $emit 和 $on 事件总线模式,因为 Vue 3 移除了 $on、$off 等方法。
一、为什么 Vue 3 不再内置事件总线?
Vue 2 中可通过 new Vue() 创建事件总线:
// Vue 2 方式
export const eventBus = new Vue();
但在 Vue 3 中:
$on、$off、$once 被移除
- 官方推荐使用 Provide/Inject、全局状态管理(Pinia/Vuex) 或第三方事件库
- 保持应用结构的清晰和可维护性
二、使用 mitt 实现事件总线(推荐)
1. 安装 mitt
npm install mitt
# 或
yarn add mitt
2. 创建事件总线
方式一:全局事件总线
// eventBus.js
import mitt from 'mitt';
const emitter = mitt();
export default emitter;
方式二:在应用层提供事件总线(依赖注入)
// main.js
import { createApp } from 'vue';
import mitt from 'mitt';
import App from './App.vue';
const emitter = mitt();
const app = createApp(App);
// 作为全局属性(不推荐大量使用)
app.config.globalProperties.$emitter = emitter;
// 或使用 Provide/Inject
app.provide('emitter', emitter);
app.mount('#app');
3. 使用示例
发送事件
<!-- ComponentA.vue -->
<template>
<button @click="sendMessage">发送消息</button>
</template>
<script setup>
import emitter from '@/eventBus';
const sendMessage = () => {
emitter.emit('message', { text: 'Hello from Component A!' });
};
</script>
接收事件
<!-- ComponentB.vue -->
<template>
<p>接收的消息:{{ message }}</p>
</template>
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
import emitter from '@/eventBus';
const message = ref('');
// 监听事件
const handleMessage = (data) => {
message.value = data.text;
};
onMounted(() => {
emitter.on('message', handleMessage);
});
// 清理监听
onUnmounted(() => {
emitter.off('message', handleMessage);
});
</script>
监听所有事件
emitter.on('*', (type, data) => {
console.log(`事件类型:${type}`, data);
});
一次性监听
emitter.once('message', (data) => {
console.log('只触发一次', data);
});
三、使用 Provide/Inject 实现事件通信
Vue 3 的响应式系统结合 Provide/Inject 也可实现类似功能:
// eventService.js
import { ref, provide, inject } from 'vue';
const createEventService = () => {
const listeners = new Map();
const emit = (event, data) => {
const eventListeners = listeners.get(event);
if (eventListeners) {
eventListeners.forEach(callback => callback(data));
}
};
const on = (event, callback) => {
if (!listeners.has(event)) {
listeners.set(event, new Set());
}
listeners.get(event).add(callback);
};
const off = (event, callback) => {
const eventListeners = listeners.get(event);
if (eventListeners) {
eventListeners.delete(callback);
}
};
return { emit, on, off };
};
// 提供事件服务
export const useEventService = () => {
const service = createEventService();
const provideEventService = () => {
provide('eventService', service);
};
const injectEventService = () => {
const service = inject('eventService');
if (!service) throw new Error('Event service not provided');
return service;
};
return { provideEventService, injectEventService };
};
四、最佳实践建议
1. 使用场景
- 简单组件通信(如通知、状态同步)
- 插件或工具库中的内部通信
- 小型项目中的快速原型开发
2. 注意事项
// 1. 及时清理监听器,避免内存泄漏
onUnmounted(() => {
emitter.off('event', handler);
});
// 2. 使用命名规范避免事件冲突
emitter.emit('user:login', data);
emitter.emit('cart:update', data);
// 3. 大型项目优先使用 Pinia 进行状态管理
3. 替代方案对比
| 方案 |
适用场景 |
优点 |
缺点 |
|---|
| mitt 事件总线 |
跨组件简单通信 |
轻量、灵活 |
不易追踪数据流 |
| Provide/Inject |
祖先-后代通信 |
Vue 原生支持 |
不适合跨分支组件 |
| Pinia/Vuex |
全局状态管理 |
可预测、可调试 |
相对较重 |
| Props/Emit |
父子组件通信 |
明确的数据流 |
无法跨层级 |
五、完整示例
// eventBus.js
import mitt from 'mitt';
const emitter = mitt();
// 可选:添加类型提示(TypeScript)
// type Events = {
// 'user-login': { id: number; name: string };
// 'notification': string;
// };
export default emitter;
// ComponentA.vue
import emitter from './eventBus';
emitter.emit('user-login', { id: 1, name: 'John' });
// ComponentB.vue
import { onMounted, onUnmounted } from 'vue';
import emitter from './eventBus';
onMounted(() => {
emitter.on('user-login', (user) => {
console.log('用户登录:', user);
});
});
总结
Vue 3 中推荐使用 mitt 作为事件总线解决方案,它小巧(200B)且功能完整。但对于复杂应用,建议优先考虑 Pinia 进行状态管理,事件总线更适合作为辅助通信手段。记住要及时清理事件监听,并合理规划事件命名空间,避免事件冲突和维护困难。