html无全局状态管理能力,组件间全局发布订阅需手动实现事件中心;document.addeventlistener不适合作为事件总线因易冲突、难清理、语义模糊、依赖dom结构;最小eventbus类仅需on/emit/off三方法;vue/react项目应优先选用mitt或context等更优方案;发布订阅仅通知变化,不替代状态同步。

HTML 本身没有全局状态管理能力,所谓“组件间全局发布订阅”,本质是靠 JavaScript 手动维护一个事件中心 —— 它不保存状态,只中转消息;真要同步状态,必须另配响应式数据源。
为什么不能直接用 document.addEventListener 做事件总线
看似简单,但实际踩坑密集:
-
document.addEventListener('user-login', handler)的事件名极易冲突,比如两个第三方库都发user-login,谁先注册谁生效,后注册的被覆盖 - 匿名函数或箭头函数传入
addEventListener后,无法用removeEventListener清理,必然内存泄漏 -
event对象自带target、stopPropagation等 DOM 层属性,和业务数据混在一起,语义模糊,调试困难 - 事件冒泡机制不是松耦合通信,它依赖 DOM 树路径,一旦结构变动,监听就失效
怎么写一个最小可用的 EventBus 类
核心就三件事:存回调、触发、删回调。几十行足够,无需框架:
class EventBus {
constructor() {
this.events = {}
}
on(type, listener) {
if (!this.events[type]) this.events[type] = []
this.events[type].push(listener)
}
emit(type, payload) {
const handlers = this.events[type] || []
handlers.forEach(fn => fn(payload))
}
off(type, listener) {
if (!this.events[type]) return
this.events[type] = this.events[type].filter(fn => fn !== listener)
}
}
注意几个硬约束:
-
on不做去重,重复添加同一函数会多次执行 —— 需要的话得自己用Set或比对引用 -
emit是同步调用,不支持异步队列;如需延迟,得手动包一层setTimeout或Promise.resolve().then() - 没实现
once,真要一次性监听,得在回调里显式调off
Vue/React 项目里还该不该手写 EventBus
通常不该 —— 生态已有更轻、更可控的替代:
- Vue 3:官方已移除
$on/$emit,推荐用mitt(200B)或tiny-emitter,它们不依赖 Vue 实例,可跨 setup 使用 - React:函数组件无实例,
Context+useReducer覆盖了大部分广播场景;真要事件驱动,用自定义useEventBusHook 比全局EventBus更安全(能自动绑定组件生命周期) - 微前端或纯 HTML 页面:才需要手写,且务必确保每个
on都配对off,尤其在组件卸载时清理
最易被忽略的一点:发布订阅永远不等于状态同步。你发 auth-change,接收方可以清缓存、重置 UI,但用户信息本身还得从一个 ref 或 store 里读 —— 消息只是通知“该刷新了”,不是搬运数据。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











