HTML无原生Pub/Sub,需用JS封装EventEmitter类,核心是维护事件名到回调数组的映射,支持on/emit/off;避免滥用DOM事件模拟,推荐轻量库或闭包私有实例。

HTML 本身不提供原生的发布订阅(Pub/Sub)机制,EventEmitter 是 Node.js 的模块,浏览器环境里没有。想在前端实现类似功能,得靠 JavaScript 自己封装,或用轻量事件系统替代。
浏览器里怎么写一个最小可用的 EventEmitter 类
核心是维护一个事件名到回调函数数组的映射,支持 on、emit、off 三个基本操作。不需要依赖框架,几十行就能搞定:
class EventEmitter {
constructor() {
this.events = {};
}
on(type, listener) {
if (!this.events[type]) this.events[type] = [];
this.events[type].push(listener);
}
emit(type, ...args) {
const handlers = this.events[type] || [];
handlers.forEach(fn => fn(...args));
}
off(type, listener) {
if (!this.events[type]) return;
this.events[type] = this.events[type].filter(fn => fn !== listener);
}
}
注意点:
-
on不做去重,重复on同一函数会多次触发 —— 如果需要,得自己加 Set 或检查引用 -
emit是同步执行,不支持异步队列或 Promise 链,如需异步,得手动包装setTimeout或Promise.resolve().then() - 没做事件 once 支持,要一次性监听就得自己在回调里调
off
为什么不该直接在 DOM 元素上滥用 addEventListener 模拟 Pub/Sub
DOM 事件系统本质是冒泡/捕获机制,不是松耦合的发布订阅。拿 document.addEventListener('data-updated', ...) 当全局事件总线,短期能跑,但隐患明显:
- 事件名容易冲突(比如两个第三方库都发
user-login) - 无法精准控制作用域,
removeEventListener必须传入**同一个函数引用**,匿名函数或箭头函数会导致内存泄漏 - DOM 事件对象带大量无关属性(
event.target、event.stopPropagation),和业务数据混在一起,语义不清
真正需要跨组件通信时,优先用独立的 EventEmitter 实例,而不是绑在 document 或 window 上。
Vue/React 项目里要不要手写 EventEmitter
通常不用 —— 生态里已有更合适的替代:
- Vue 2:实例自带
$on/$emit/$off,但仅限于父子组件通信;Vue 3 废除了这些 API,推荐用mitt或tiny-emitter这类 200B 级别的库 - React:函数组件无实例,Context + useReducer 已覆盖大部分状态广播场景;真要事件驱动,
useEventBus(自定义 Hook)比全局EventEmitter更可控 - 如果只是模块内通信(比如一个工具函数集合需要通知状态变更),直接用闭包私有变量 + 一个本地
EventEmitter实例更干净,避免污染全局
最常被忽略的一点:事件参数别传大对象或 DOM 节点引用,容易引发意外的内存滞留;尽量传 plain object 或 ID 字符串,让监听方按需取数。
前端入门到VUE实战笔记:立即使用
在学习笔记中,你将探索 前端 的入门与实战技巧!











