编码母题 / P1 / Node 与 JavaScript / 02
事件发布订阅器
先读清题意与边界,独立尝试,再展开思路并对照可运行的参考代码。
重要程度高价值难度中等预计完成30–40 分钟
题目是什么
实现 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
/*
* 题目:事件发布订阅器
* 要求:实现 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 遍历监听器快照;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 项快照。