如何在 Vue 中实现事件总线 EventBus?
Vue 2 里用一个空 Vue 实例做中央事件通道,组件通过它发事件和听事件:
javascript// event-bus.js import Vue from 'vue'; export const EventBus = new Vue();
发送端 EventBus.$emit,接收端 EventBus.$on,销毁前必须 EventBus.$off 解绑——忘了这一步就是内存泄漏。
典型场景:登录弹窗成功后通知导航栏更新头像,两个组件隔了好几层,props 传递不现实,EventBus 几行代码搞定。
Vue 3 移除了 $on/$off,官方推荐用 mitt 替代,写法几乎一样:
javascriptimport mitt from 'mitt'; export const bus = mitt(); // 发送 bus.emit('login-success', { user: 'xxx' }); // 监听 bus.on('login-success', handler); // 移除 bus.off('login-success', handler);
追问
Vue 3 为什么移除 $on/$off?
Vue 3 的响应式体系更推崇单向数据流和显式状态管理。EventBus 是隐式依赖——组件 A emit 一个事件,组件 B 在哪监听的完全不知道,出了 bug 追踪困难。Vue 团队认为这种模式弊大于利,干脆砍掉。如果确实需要事件模式,mitt 只有 200 字节,功能够用。
EventBus 和 Pinia 怎么选?
EventBus 适合"通知型"通信——A 告诉 B 发生了什么事,不需要持久化数据。Pinia 适合"状态型"通信——多个组件共享同一份数据,需要 devtools 调试和变更追溯。事件一多就别用 EventBus,散落在各组件里的 on/off 根本理不清,直接上 Pinia。
为什么必须 $off?忘了会怎样?
组件销毁了但 EventBus 实例还活着,回调引用还在,下次 emit 还会执行——操作一个已销毁组件的 this,轻则报错重则数据错乱。更严重的是 EventBus 持有回调闭包,闭包引用了组件实例,GC 无法回收,这就是内存泄漏。Vue 2 里如果用了箭头函数做 handler,$off 时传引用也解不掉,必须用命名函数。
EventBus 事件名冲突怎么办?
大型项目里十几个组件各自 emit/on,事件名撞了根本不会报错——只是 A 的数据莫名其妙到了 B。解法:统一在常量文件里定义事件名,禁止硬编码字符串。更好的做法是如果事件超过 5-6 个就别用 EventBus 了,换 Pinia。
TypeScript 下 mitt 怎么做类型安全?
mitt 支持泛型约束事件 map:
typescripttype Events = { 'login-success': { user: string }; 'logout': undefined; }; const bus = mitt<Events>(); bus.emit('login-success', { user: 'xxx' }); // ✓ bus.emit('wrong-name', {}); // ✗ 类型报错
这样事件名和 payload 类型都有编译期检查,比 Vue 2 的 EventBus 安全得多。