闭包通过在工厂函数内声明handlers数组并返回操作它的函数,确保订阅者列表私有且持久;事件名作键、回调作引用值,解绑需严格比对函数实例;emit中用slice()复制数组避免遍历时被修改。

闭包怎么保存订阅者列表
闭包的核心作用是让内部函数持续访问外部函数的变量,而不被垃圾回收。发布订阅模式里,订阅者列表 handlers 必须私有且持久——不能暴露给外部直接修改,也不能在每次调用时重置。
常见错误是把 handlers 声明在全局或模块顶层,导致多个实例共享同一组回调;或者写成普通对象方法,this 绑定错、状态丢失。
- 必须在工厂函数(如
createEventBus)内部声明handlers数组 - 所有操作(
on、emit、off)都通过返回的闭包函数访问它 - 避免使用
class或this,否则闭包意义被削弱,容易意外暴露状态
function createEventBus() {
const handlers = {};
return {
on(event, fn) { /* ... */ },
emit(event, data) { /* ... */ },
off(event, fn) { /* ... */ }
};
}
如何正确处理事件名和回调的绑定与解绑
事件名是字符串键,回调是引用值,解绑(off)失败最常见原因是:用匿名函数订阅后试图用另一个匿名函数解绑,或没比对函数引用而是比对内容。
闭包模式下,handlers[event] 存的是函数引用数组,off 时必须用 === 判断是否为同一函数实例。
- 禁止在
on里传入内联箭头函数:bus.on('click', () => {...})→ 后续无法off -
off时要遍历handlers[event],用findIndex+===匹配,再用splice - 支持通配符(如
'user.*')需额外解析逻辑,不在基础闭包范围内,容易引入正则或字符串匹配开销
const handler = (data) => console.log(data);
bus.on('login', handler);
bus.off('login', handler); // ✅ 可行
bus.off('login', (d) => console.log(d)); // ❌ 不会生效
为什么 emit 里要用 slice() 遍历回调列表
执行 emit 时,回调函数内部可能调用 on 或 off,直接遍历原数组会导致索引错乱、漏调或重复调用——这是闭包封装下最容易被忽略的并发陷阱。
不是所有场景都需要深拷贝,但 slice() 是轻量安全的选择:它生成一个新数组引用,不影响原 handlers[event] 的结构,也不触发深克隆开销。
- 不推荐
for...of直接遍历handlers[event] - 更不推荐
forEach—— 它无法中断,且同样受中途修改影响 - 如果业务要求“同步阻塞式”执行(比如权限校验链),需另加控制流,闭包本身不解决这个问题
emit(event, data) {
const fns = handlers[event];
if (!fns || fns.length === 0) return;
// ✅ 安全复制一份引用
fns.slice().forEach(fn => fn(data));
}
Node.js 与浏览器环境下的兼容性注意点
纯闭包实现的发布订阅不依赖任何 API,所以 createEventBus 在 Node.js(CommonJS/ESM)、浏览器(ES6+)甚至微信小程序里都能跑。但实际集成时容易踩两个坑:
- 模块导出方式不一致:ESM 中默认导出要写
export default createEventBus,CommonJS 要module.exports = createEventBus,混用会报TypeError: xxx is not a function - 如果后续想加异步支持(如
emitAsync),需手动处理 Promise 拒绝,闭包本身不提供错误边界 —— 错误会直接抛到调用栈顶层,可能中断后续回调
真正麻烦的从来不是闭包怎么写,而是当 emit 被高频调用、或某个 handler 执行超时/死循环时,你根本没法从闭包内部做超时中断或资源清理。那已经超出模式本身,得靠运行时机制兜底。










