功能说明
该示例实现一个不依赖 Node 的事件发射器,重点补齐 once 与 off 的语义:once 注册的监听器在首次触发前先把自己移除,因此即使回调内部再次 emit 同一事件也不会重复执行;off 既可以用包装函数移除,也可以用最初传入的原始回调移除。emit 在派发前复制监听器集合,允许回调中安全地增删监听器。适用于浏览器与 Node 共用的小型事件总线、插件生命周期钩子等场景。
关键代码
once 把原始监听器包进 wrapper,并在 wrapper 上记录 original 属性;触发时先调用 off 删除 wrapper,再执行原回调。off 遍历当前集合,同时匹配 entry 与 entry.original,因此两种引用都能删除。emit 使用展开运算符生成快照,避免遍历过程中修改 Set 导致漏发。on 返回的取消函数直接复用 off,调用方可保存以便组件卸载时清理。所有注册入口都会校验事件名与回调类型。
使用注意
派发是同步的,任一监听器抛错都会中断后续监听器,需要隔离时应自行 try/catch。once 只保证同一 wrapper 不再触发,若业务用同一原始函数注册多次,每次仍各触发一次。removeAllListeners 不传事件名会清空全部监听,适合测试重置。该实现没有通配符事件、异步派发或最大监听数限制,长期运行的服务应结合监听器计数做泄漏排查。
验证方式
用 node:test 编写测试:注册 once 后连续 emit 两次,断言只调用一次且 listenerCount 归零;用原始回调调用 off,确认已注册监听器被移除;在监听器内部再 emit 同一事件,验证快照派发不会无限递归;最后检查非法事件名与非法回调会抛出 TypeError。
代码示例
export class EventEmitter {
#listeners = new Map();
on(event, listener) {
if (typeof event !== "string" || event.length === 0) {
throw new TypeError("event must be a non-empty string");
}
if (typeof listener !== "function") {
throw new TypeError("listener must be a function");
}
let bucket = this.#listeners.get(event);
if (!bucket) {
bucket = new Set();
this.#listeners.set(event, bucket);
}
bucket.add(listener);
return () => this.off(event, listener);
}
once(event, listener) {
const wrapper = (...args) => {
this.off(event, wrapper);
listener(...args);
};
wrapper.original = listener;
return this.on(event, wrapper);
}
off(event, listener) {
const bucket = this.#listeners.get(event);
if (!bucket) return false;
let removed = false;
for (const entry of bucket) {
if (entry === listener || entry.original === listener) {
bucket.delete(entry);
removed = true;
}
}
if (bucket.size === 0) this.#listeners.delete(event);
return removed;
}
emit(event, ...args) {
const bucket = this.#listeners.get(event);
if (!bucket) return false;
for (const listener of [...bucket]) listener(...args);
return true;
}
listenerCount(event) {
return this.#listeners.get(event)?.size ?? 0;
}
removeAllListeners(event) {
if (event === undefined) this.#listeners.clear();
else this.#listeners.delete(event);
}
}