JavaScript 的 Map 本身无事件机制,但可封装类型友好的 Pub/Sub 总线:用 Map 存储事件名到回调数组的映射,支持 on/emit/off/once 及可选命名空间匹配。

JavaScript 中的 Map 本身不提供事件机制,但可以基于它手动封装一个轻量、类型友好的发布订阅(Pub/Sub)事件总线,核心是用 Map 存储事件名到回调函数列表的映射。
用 Map 存储事件监听器
每个事件名(字符串或 Symbol)作为 key,对应一个数组(存放所有订阅该事件的回调函数)作为 value。相比对象字面量,Map 支持任意类型 key、可遍历、有明确 size,更适合动态管理事件。
- 初始化时创建空
Map:new Map() - 订阅(on):若事件名不存在,先设为空数组;再用
push加入回调 - 发布(emit):查出对应数组,遍历执行,传入参数
- 取消订阅(off):找到数组,用
splice或filter移除指定回调(需保留引用)
支持一次性监听(once)
在内部封装一个包装函数,执行后自动调用 off。关键点是把包装后的函数存进 Map,而不是原始函数——否则无法精准移除。
- 定义
once方法:生成闭包函数,内含对原始回调和事件名的引用 - 在包装函数执行完立即调用
this.off(eventName, wrapper) - 确保
off能通过函数引用匹配并删除(不能用匿名函数)
支持事件命名空间或通配符(可选增强)
原生 Map 不支持模糊匹配,但可通过扩展实现简单模式,比如用点号分隔的命名空间("user.login"),或支持 "*" 全局监听。
立即学习“Java免费学习笔记(深入)”;
- 在
emit时,除了匹配精确事件名,也检查"*"和带通配前缀的键(如"user.*") - 遍历 Map 所有 key,用字符串方法(如
startsWith、includes)做轻量匹配 - 注意性能:事件量大时不建议频繁全量遍历,生产环境可用专门的事件树结构替代
完整可运行示例
以下是一个最小可用实现,无依赖、可直接使用:
class EventBus {
constructor() {
this.events = new Map();
}
on(eventName, callback) {
if (!this.events.has(eventName)) {
this.events.set(eventName, []);
}
this.events.get(eventName).push(callback);
}
once(eventName, callback) {
const wrapper = (...args) => {
callback(...args);
this.off(eventName, wrapper);
};
this.on(eventName, wrapper);
}
emit(eventName, ...args) {
const callbacks = this.events.get(eventName);
if (callbacks) {
callbacks.forEach(cb => cb(...args));
}
}
off(eventName, callback) {
const callbacks = this.events.get(eventName);
if (callbacks) {
const index = callbacks.indexOf(callback);
if (index > -1) {
callbacks.splice(index, 1);
}
}
}
clear(eventName) {
if (eventName) {
this.events.delete(eventName);
} else {
this.events.clear();
}
}
}
// 使用示例
const bus = new EventBus();
bus.on('data', (msg) => console.log('收到:', msg));
bus.once('init', () => console.log('只执行一次'));
bus.emit('data', 'hello');
bus.emit('init'); // 触发后自动解绑


















