编码母题 / P1 / Node 与 JavaScript / 02

事件发布订阅器

先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。

重要程度高价值难度中等预计完成30–40 分钟

难度按 LeetCode 简单/中等/困难三档估计;工程题并非 LeetCode 官方题目。用时指独立编码并检查主要边界的参考时间,因经验而异。

题目是什么

实现 on/off/once/emit/listenerCount。监听器按注册顺序同步执行;emit 期间增删监听器不改变本轮快照,once 在调用前注销以处理重入。

输入输出示例

once 回调里再次 emit 同一事件,原 once 回调仍只能执行一次。

练习起手式与验证

复制代码后补全空方法,再运行下方三组验证;复杂题所需的辅助类型与输入准备一并放在代码块中。

/*
 * 题目:事件发布订阅器
 * 要求:实现 on/off/once/emit/listenerCount。
 * 监听器按注册顺序同步执行;emit 期间增删监听器不改变本轮快照,once
 * 在调用前注销以处理重入。
 * 示例:once 回调里再次 emit 同一事件,原 once 回调仍只能执行一次。
 */
class EventEmitter {
  constructor() {}
  on(event, listener) {}
  once(event, listener) {}
  off(event, listener) {}
  emit(event, ...args) {}
  listenerCount(event) {}
}

const emitter = new EventEmitter();
const values = [];
const a = (v) => {
  values.push(`a${v}`);
  emitter.on("x", () => values.push("late"));
};
emitter.on("x", a).once("x", (v) => {
  values.push(`once${v}`);
  emitter.emit("nested", v);
});
emitter.emit("x", 1);
console.log(values); // 预期:["a1", "once1"]
emitter.emit("x", 2);
console.log(values); // 预期:["a1", "once1", "a2", "late"]
emitter.off("x", a);
console.log(emitter.listenerCount("x")); // 预期:2

解题思路

展开思路、不变量与常见边界

关键切入点:每次 emit 遍历监听器快照;once 包装器保存原函数,便于 off 使用原函数取消。

不变量:events 中只保存当前有效监听器;once 包装器保留 original 引用,使 off(event, original) 也能移除。emit 时间 O(k),注册 O(1),移除 O(k)。 Node 原生 EventEmitter 对 error 有特殊崩溃语义,本练习不模拟该约定。

参考代码与验证

“关键机制”注释用于解释实现,下载链接提供未经页面注释修改的原始 JS 文件。

// 关键机制:每次 emit 遍历监听器快照;once 包装器保存原函数,便于 off 使用原函数取消。
class EventEmitter {
  constructor() {
    this.events = new Map();
  }
  on(event, listener) {
    if (typeof listener !== "function")
      throw new TypeError("listener must be a function");
    const list = this.events.get(event) || [];
    list.push(listener);
    this.events.set(event, list);
    return this;
  }
  once(event, listener) {
    const wrapper = (...args) => {
      this.off(event, wrapper);
      return listener.apply(this, args);
    };
    wrapper.original = listener;
    return this.on(event, wrapper);
  }
  off(event, listener) {
    const list = this.events.get(event);
    if (!list) return this;
    const next = list.filter(
      (item) => item !== listener && item.original !== listener,
    );
    if (next.length) this.events.set(event, next);
    else this.events.delete(event);
    return this;
  }
  emit(event, ...args) {
    const list = this.events.get(event);
    if (!list) return false;
    for (const listener of [...list]) listener.apply(this, args);
    return true;
  }
  listenerCount(event) {
    return this.events.get(event)?.length || 0;
  }
}

// 三组以上可直接运行的验证。
const emitter = new EventEmitter();
const values = [];
const a = (v) => {
  values.push(`a${v}`);
  emitter.on("x", () => values.push("late"));
};
emitter.on("x", a).once("x", (v) => {
  values.push(`once${v}`);
  emitter.emit("nested", v);
});
emitter.emit("x", 1);
console.log(values); // 预期:["a1", "once1"]
emitter.emit("x", 2);
console.log(values); // 预期:["a1", "once1", "a2", "late"]
emitter.off("x", a);
console.log(emitter.listenerCount("x")); // 预期:2

复杂度分析

时间复杂度:emit O(k),on O(1),off O(k)。发布时复制并遍历该事件的 k 个监听器;注销需查找目标。

空间复杂度:O(L)。保存全部 L 个已注册监听器,发布时另建至多 k 项快照。

下载原始 JS 文件