事件总线是一种经典的“发布-订阅”通信方案,允许任意两个组件之间直接传递事件,无需关心它们在组件树中的层级关系。在 Vue 2 中,每个实例都自带 $on、$emit、$off 方法,开发者通常会额外创建一个空的 Vue 实例作为中央事件总线:const bus = new Vue()。
在 Vue 3 中的实现
Vue 3 移除了 $on、$off 等事件接口(官方认为这些 API 容易导致代码组织混乱),但事件总线的思想仍然可以通过第三方库或手写实现。当前最主流、最轻量的方案是使用 mitt(仅 200 字节):
// eventBus.js
import mitt from 'mitt'
export const bus = mitt()
发送事件的组件:
<script setup>
import { bus } from './eventBus'
bus.emit('user-login', { name: 'Alice', role: 'admin' })
</script>
接收事件的组件:
<script setup>
import { bus } from './eventBus'
import { onMounted, onUnmounted } from 'vue'
const handleLogin = (payload) => {
console.log('用户登录:', payload)
}
onMounted(() => {
bus.on('user-login', handleLogin)
})
onUnmounted(() => {
bus.off('user-login', handleLogin) // 必须清理,防止内存泄漏
})
</script>
事件总线的局限性
- 数据流向不清晰,调试困难:事件在全局总线上“满天飞”,调用
emit的地方和监听on的地方完全分离,项目规模一大就很难追踪某个事件从哪发出、被谁消费。出问题时,排查流程像是在漆黑的房间里抓猫。 - 命名冲突风险高:全局事件名是纯字符串,没有命名空间保护。两个开发者不小心都用了
'data-update'事件,就会互相干扰,且编译阶段无法报错。 - 容易产生内存泄漏:监听事件后必须手动在组件销毁前调用
off移除监听,否则组件被卸载后事件处理函数依然存活,形成“幽灵监听”。在大型项目中,遗忘清理是高频错误。 - 不适合复杂状态管理:事件总线只负责传递“发生了什么事”,不持有任何状态。当通信需要依赖于“当前的状态是什么”时(比如购物车数量、用户登录态),事件总线就力不从心,强行使用会让状态散落在各处难以管理。
正因为这些局限,在 Vue 生态中,事件总线主要用于小规模、非关键路径的跨组件通信(例如某个无关紧要的侧边栏折叠状态通知)。一旦通信逻辑开始变多、变复杂,更推荐的方案是采用全局状态管理库(Pinia)或依赖注入(provide/inject),它们提供了更可预测的数据流和更好的维护性。