组件卸载时,如果没有及时注销事件总线,往往会造成内存泄漏。这是因为事件监听器形成的闭包会持续持有组件实例的引用。Vue 本身不会管理自定义 eventBus 的生命周期,因此必须手动执行 $off 清理。更推荐使用 mitt 的 on() 返回的 off() 函数,并在 onUnmounted 中完成精确解绑,或者直接采用 provide/inject、Pinia 订阅等更稳妥的组件通信方案。

组件销毁或卸载时若没有注销全局事件总线,是前端项目中常见的内存泄漏来源之一——它会让整个组件实例持续被监听器闭包引用,最终无法被垃圾回收机制正确释放。
为什么 eventBus 注册后必须手动 $off?
Vue 本身并不会负责管理 mitt、tiny-emitter 这类自定义事件总线,就连手动挂载的 Vue.prototype.$bus 也同样如此。一旦通过$on注册了监听器,回调函数就会生成闭包,从而隐式持有组件上下文,例如 this、ref,以及 setup 中定义的变量。只要没有主动调用$off,这条引用链就会一直保留。
即便组件已经从 DOM 中移除,或者响应式依赖已经销毁,eventBus 内部依然可能保存着指向已卸载组件的回调函数,导致相关内存始终无法释放。
正确注销方式:保存 off 函数并统一在 onUnmounted 中执行
像 mitt 这样的轻量级事件库,on() 方法通常会返回一个off() 函数,这也是最安全、最准确的事件解绑方式:
- 在 setup 中使用 ref 缓存 off 函数:const off = ref(null)
- 注册事件时进行赋值:off.value = emitter.on('user:update', handler)
- 在 onUnmounted 生命周期中执行:off.value?.()
不建议直接使用 emitter.off('user:update', handler) —— 因为这种写法要求传入完全一致的函数引用,而箭头函数或每次渲染时新建的函数都可能导致匹配失败,进而让解绑失效。
替代方案:优先改用 provide / inject 或 Pinia 订阅
事件总线本质上属于全局强引用的通信方式,使用不当很容易失控。更稳定、更易维护的组件通信方案包括:
- provide / inject:适合父子或祖孙组件通信,天然受组件生命周期管理约束,通常不需要手动清理
- Pinia store 的 actions 或 $subscribe:通过 store 驱动状态变化,监听通常会随着组件卸载而失效(尤其是在配合 store.$onAction 使用时)
- 如果业务上必须使用 eventBus,建议进一步封装为 useEventBus Composable,在内部实现自动绑定与自动解绑,减少重复错误
额外注意:避免在总线回调中触发副作用泄漏
即使已经成功执行 $off,如果监听器内部还开启了定时器、发送了请求,或者创建了 watch,这些额外副作用仍然需要分别清理:
- 监听器中使用了 setTimeout?记得在 onUnmounted 中同时清除对应的 timer
- 监听器中调用了 watchEffect?请保存其返回的 stop 函数,并在组件卸载时执行
- 监听器中发起了 fetch 请求?建议配合 AbortController,并在 onUnmounted 中调用 abort
