【力扣】2694. 事件发射器
题目
设计一个 EventEmitter 类。这个接口与 Node.js 或 DOM 的 Event Target 接口相似,但有一些差异。EventEmitter 应该允许订阅事件和触发事件。
你的 EventEmitter 类应该有以下两个方法:
- subscribe - 这个方法接收两个参数:一个作为字符串的事件名和一个回调函数。当事件被触发时,这个回调函数将被调用。 一个事件应该能够有多个监听器。当触发带有多个回调函数的事件时,应按照订阅的顺序依次调用每个回调函数。应返回一个结果数组。你可以假设传递给
subscribe的回调函数都不是引用相同的。subscribe方法还应返回一个对象,其中包含一个unsubscribe方法,使用户可以取消订阅。当调用unsubscribe方法时,回调函数应该从订阅列表中删除,并返回 undefined。 - emit - 这个方法接收两个参数:一个作为字符串的事件名和一个可选的参数数组,这些参数将传递给回调函数。如果没有订阅给定事件的回调函数,则返回一个空数组。否则,按照它们被订阅的顺序返回所有回调函数调用的结果数组。
示例 1:
输入:
actions = ["EventEmitter", "emit", "subscribe", "subscribe", "emit"],
values = [[], ["firstEvent", "function cb1() { return 5; }"], ["firstEvent", "function cb1() { return 5; }"], ["firstEvent"]]
输出:[[],["emitted",[]],["subscribed"],["subscribed"],["emitted",[5,6]]]
解释:
const emitter = new EventEmitter();
emitter.emit("firstEvent"); // [], 还没有订阅任何回调函数
emitter.subscribe("firstEvent", function cb1() { return 5; });
emitter.subscribe("firstEvent", function cb2() { return 6; });
emitter.emit("firstEvent"); // [5, 6], 返回 cb1 和 cb2 的输出
示例 2:
输入:
actions = ["EventEmitter", "subscribe", "emit", "emit"],
values = [[], ["firstEvent", "function cb1(...args) { return args.join(','); }"], ["firstEvent", [1,2,3]], ["firstEvent", [3,4,6]]]
输出:[[],["subscribed"],["emitted",["1,2,3"]],["emitted",["3,4,6"]]]
解释:注意 emit 方法应该能够接受一个可选的参数数组。
const emitter = new EventEmitter();
emitter.subscribe("firstEvent, function cb1(...args) { return args.join(','); });
emitter.emit("firstEvent", [1, 2, 3]); // ["1,2,3"]
emitter.emit("firstEvent", [3, 4, 6]); // ["3,4,6"]
示例 3:
输入:
actions = ["EventEmitter", "subscribe", "emit", "unsubscribe", "emit"],
values = [[], ["firstEvent", "(...args) => args.join(',')"], ["firstEvent", [1,2,3]], [0], ["firstEvent", [4,5,6]]]
输出:[[],["subscribed"],["emitted",["1,2,3"]],["unsubscribed",0],["emitted",[]]]
解释:
const emitter = new EventEmitter();
const sub = emitter.subscribe("firstEvent", (...args) => args.join(','));
emitter.emit("firstEvent", [1, 2, 3]); // ["1,2,3"]
sub.unsubscribe(); // undefined
emitter.emit("firstEvent", [4, 5, 6]); // [], 没有订阅者
示例 4:
输入:
actions = ["EventEmitter", "subscribe", "subscribe", "unsubscribe", "emit"],
values = [[], ["firstEvent", "x => x + 1"], ["firstEvent", "x => x + 2"], [0], ["firstEvent", [5]]]
输出:[[],["subscribed"],["subscribed"],["unsubscribed",0],["emitted",[7]]]
解释:
const emitter = new EventEmitter();
const sub1 = emitter.subscribe("firstEvent", x => x + 1);
const sub2 = emitter.subscribe("firstEvent", x => x + 2);
sub1.unsubscribe(); // undefined
emitter.emit("firstEvent", [5]); // [7]
提示:
1 <= actions.length <= 10values.length === actions.length- 所有测试用例都是有效的。例如,你不需要处理取消一个不存在的订阅的情况。
- 只有 4 种不同的操作:
EventEmitter、emit、subscribe和unsubscribe。EventEmitter操作没有参数。 emit操作接收 1 或 2 个参数。第一个参数是要触发的事件名,第二个参数传递给回调函数。subscribe操作接收 2 个参数,第一个是事件名,第二个是回调函数。unsubscribe操作接收一个参数,即之前进行订阅的顺序(从 0 开始)。
解决方案
概述
如果我们需要设计一个 EventEmitter 类,该类允许订阅事件并触发它们。EventEmitter 应该具有以下两个方法:
ubscribe(eventName, callback):此方法接受事件名称(字符串)和回调函数作为参数。当事件被触发时,将调用回调函数。一个事件可以有多个监听器。回调应该按照它们被订阅的顺序被调用。subscribe 方法应该返回一个带有 unsubscribe 方法的对象,用于从订阅列表中移除回调。emit(eventName, args):此方法接受事件名称(字符串)和一个可选的参数数组。它应该触发与事件名称相关联的回调,将提供的参数传递给每个回调。如果没有回调订阅给定事件,该方法应该返回一个空数组。否则,它应该返回一个数组,其中包含按照它们被订阅的顺序调用的所有回调的结果。
在继续之前,让我们理解一些术语的含义:
事件和事件驱动编程
事件代表程序中发生的事情。例如,当用户点击按钮时,它会触发一个"点击"事件。
事件驱动编程侧重于响应事件,而不是遵循固定的步骤序列。它允许程序响应用户交互和外部变化。
示例: 想象一个游戏,当玩家按下箭头键时,玩家角色会移动。游戏使用事件来检测按键并相应地更新角色的位置。
EventEmitter
EventEmitter 是一个在程序中管理事件的工具或类。它允许组件订阅事件并在这些事件发生时接收通知。
示例: 将 EventEmitter 想象成一个广播不同类型的节目(事件)的广播电台,听众(组件)可以调整频道以收听他们感兴趣的特定节目。
订阅和回调
订阅允许组件表达对特定事件的兴趣。它们指定他们想要监听的事件。
回调,也称为事件处理程序,是在订阅的事件发生时执行的函数。
示例: 在消息应用中,用户可以订阅"newMessage"事件,以在接收新消息时收到通知。回调函数可以在屏幕上显示消息。
js
// 处理新消息的回调函数
function handleMessageReceived(message) {
console.log("收到新消息:", message);
}
// 将回调函数订阅到"newMessage"事件
eventEmitter.subscribe("newMessage", handleMessageReceived);
回调执行顺序
- 当多个监听器订阅同一事件时,回调将按照它们被订阅的顺序执行。
- 示例: 想象一个社交媒体应用,用户可以点赞一篇帖子。每个赞都触发"postLiked"事件,所有已订阅的回调应按照它们注册的顺序执行。
从事件中取消订阅
- 当组件不再希望接收事件通知时,可以取消或移除订阅。
- 示例: 在通知系统中,用户可能在配置了他们的首选项后希望取消订阅电子邮件通知。
js
// 订阅事件的回调函数并获取取消订阅方法
const subscription = eventEmitter.subscribe("eventName", callback);
// 通过调用取消订阅方法取消事件订阅
subscription.unsubscribe();
事件参数
事件可以携带额外的信息或数据,称为事件参数,这些参数传递给回调函数。
示例: 在天气应用中,"weatherUpdate"事件可能包含诸如温度、湿度和天气状况等参数。回调函数可以使用这些参数来更新用户界面。
js
// 用于处理天气更新的回调函数
function handleWeatherUpdate(weatherData) {
console.log("温度:", weatherData.temperature);
console.log("湿度:", weatherData.humidity);
}
// 将回调函数订阅到"weatherUpdate"事件
eventEmitter.subscribe("weatherUpdate", handleWeatherUpdate);
返回值
回调可以执行基于其功能的操作并返回值。
示例: 在计算器应用程序中,订阅"calculate"事件的回调函数可能接收数字和操作等参数。它可以执行计算并返回结果。
js
// 用于处理计算的回调函数
function handleCalculation(numbers, operation) {
if (operation === "add") {
return numbers.reduce((a, b) => a + b, 0);
} else if (operation === "multiply") {
return numbers.reduce((a, b) => a * b, 1);
}
}
// 将回调函数订阅到"calculate"事件
eventEmitter.subscribe("calculate", handleCalculation);
用例
用户界面 (UI) 交互
在 Web 开发中,EventEmitter 可以用于处理用户交互,如按钮点击、表单提交或菜单选择。组件可以订阅这些事件,当事件被触发时执行适当的操作或更新。
js
// 创建一个 EventEmitter 实例
const eventEmitter = new EventEmitter();
// 订阅按钮点击事件
eventEmitter.subscribe("buttonClick", () => {
console.log("按钮被点击!");
});
// 触发按钮点击事件
eventEmitter.emit("buttonClick");
异步操作
在处理异步操作,如从 API 获取数据或处理数据库查询时,可以使用EventEmitter通知组件或模块这些操作的完成或状态。已订阅的回调可以处理返回的数据或
触发后续操作。
js
// 创建一个 EventEmitter 实例
const eventEmitter = new EventEmitter();
// 模拟异步操作
function fetchData() {
setTimeout(() => {
const data = "一些获取的数据";
// 触发带有获取的数据的事件
eventEmitter.emit("dataFetched", data);
}, 2000);
}
// 订阅 dataFetched 事件
eventEmitter.subscribe("dataFetched", (data) => {
console.log("获取的数据:", data);
});
// 触发异步操作
fetchData();
自定义事件驱动系统
EventEmitter 可以用于构建特定应用需求的自定义事件驱动系统。例如,在游戏引擎中,EventEmitter 可以用于管理事件,如玩家移动、碰撞检测或游戏状态变化。组件,如游戏对象或用户界面元素,可以订阅这些事件并做出相应的响应。
js
// 创建一个 EventEmitter 实例
const eventEmitter = new EventEmitter();
// 游戏状态更改事件
eventEmitter.subscribe("gameStateChange", (newState) => {
console.log("游戏状态变更为:", newState);
});
// 玩家移动事件
eventEmitter.subscribe("playerMovement", (movement) => {
console.log("玩家移动:", movement);
});
// 触发游戏事件
eventEmitter.emit("gameStateChange", "start");
eventEmitter.emit("playerMovement", "left");
日志和错误处理
EventEmitter 可以用于处理日志和错误事件。已订阅的回调可以捕获错误事件,将其记录到文件或控制台,并执行错误处理任务,如发送错误报告或向用户显示错误消息。
js
// 创建一个 EventEmitter 实例
const eventEmitter = new EventEmitter();
// 错误事件
eventEmitter.subscribe("error", (errorMessage) => {
console.error("发生错误:", errorMessage);
});
// 日志事件
eventEmitter.subscribe("log", (message) => {
console.log("日志消息:", message);
});
// 触发日志和错误事件
eventEmitter.emit("error", "出现了错误!");
eventEmitter.emit("log", "信息:应用程序已启动。");
事件驱动架构
EventEmitter 是事件驱动架构中的一个基本构建块。它通过允许组件通过事件进行通信来实现组件之间的松耦合和解耦。这在大规模应用程序中促进了模块化和可伸缩性。
方法 1:使用数组
概述:
当事件被触发时,我们可以检查是否有任何已订阅该事件的处理程序,方法是访问当前事件。如果没有处理程序,将返回一个空数组,表示未执行任何回调。
如果有处理程序,我们可以使用 map 方法迭代处理程序数组。对于每个处理程序,我们可以使用展开运算符(...args)调用相应的回调函数。最终,将每个回调调用的结果收集并返回为数组。
算法:
EventEmitter类定义了一个构造方法。构造函数初始化了一个名为 events 的空对象,用于存储事件订阅。该对象将事件名称作为键,相应的回调函数数组作为值。- subscribe 方法用于订阅事件。它接受两个参数:event(事件名称作为字符串)和
cb(在事件触发时要调用的回调函数)。 - 在 subscribe 方法内部:
- 我们检查是否有当前事件的现有处理程序,方法是访问
this.events[event]。 - 如果没有处理程序,我们可以使用
nullish合并运算符 ?? 初始化一个空数组。Nullish运算符评估其左侧的表达式,并且如果该值为 null 或 undefined,则返回右侧的表达式。 - 然后,我们可以将提供的回调函数 cb 推送到处理程序数组
this.events[event]中。
- 我们检查是否有当前事件的现有处理程序,方法是访问
- subscribe 方法返回一个带有 unsubscribe 方法的对象。unsubscribe 方法是一个箭头函数,用于从与相应事件关联的处理程序数组中移除订阅的回调函数。
- emit 方法用于触发事件。它接受两个参数:event(事件名称作为字符串)和可选的 args 数组,其中包含要传递给回调函数的参数。
- 在 emit 方法内部:
- 我们检查是否在
this.events中存在订阅到当前事件的处理程序。 - 如果没有,返回一个空数组 [],表示未执行任何回调。
- 如果有处理程序:
- 我们使用 map 方法迭代处理程序数组(
this.events[event])。 - 对于每个处理程序,我们使用展开运算符
(...args)调用回调函数(f)并将提供的参数传递给它。 - 最后,将每个回调函数调用的结果收集并返回为数组。
- 我们使用 map 方法迭代处理程序数组(
- 我们检查是否在
实现:
js
class EventEmitter {
constructor() {
this.events = {};
}
subscribe(event, cb) {
this.events[event] = this.events[event] ?? [];
this.events[event].push(cb);
return {
unsubscribe: () => {
this.events[event] = this.events[event].filter(f => f !== cb);
// 为避免内存泄漏,添加清理条件
if (this.events[event].length === 0) { delete this.events[event] }
},
};
}
emit(event, args = []) {
if (!(event in this.events)) return [];
return this.events[event].map(f => f(...args));
}
}
复杂度分析:
- 时间复杂度:对于
subscribe:O(1),对于unsubscribe和emit:O(n),其中 n 表示已订阅事件的回调数。 - 空间复杂度:O(n),其中 n 表示已订阅事件的回调数。
方法 2:使用 Set
概述
我们可以创建一个对象来存储每个事件,
因为相同事件可以包含许多不同的回调函数,我们可以使用 Set 而不是数组来存储相同事件的每个不同的回调。
算法
EventEmitter类定义了一个构造方法。构造函数初始化了一个名为events的空对象,用于存储事件订阅。该对象将事件名称作为键,包含与该事件相关的回调函数集的 Set 对象作为值。subscribe方法用于订阅事件。它接受两个参数:event(事件名称作为字符串)和cb(在事件触发时要调用的回调函数)。- 在 subscribe 方法内部:
- 我们首先检查事件对象是否不具有指定事件作为自有属性。如果没有,我们使用新的 Set 包含回调函数来初始化它。Set 数据结构确保不会添加重复的回调。
- 如果事件已经存在于事件对象中,我们将回调函数添加到与该事件关联的现有回调集中。
- subscribe 方法返回一个带有 unsubscribe 方法的对象。调用此方法会将回调函数从特定事件的订阅集中删除。
- emit 方法用于触发事件。它接受事件名称(作为字符串的事件名称)和可选的参数数组(包含要传递给回调函数的参数)。
- 在 emit 方法内部:首先,我们检查事件对象中是否存在指定事件。如果不存在,返回一个空数组,表示没有执行任何回调。
- 我们创建一个空的结果数组来存储回调函数调用的结果。
- 使用
forEach方法,我们遍历与事件关联的回调函数集中的每个回调函数。 - 对于每个回调函数,我们使用展开运算符
(...args)调用它。 - 将每个回调函数调用的结果推入结果数组。
- 最后,返回包含所有回调函数调用结果的结果数组。
实现
js
class EventEmitter {
constructor() {
this.events = {};
}
subscribe(event, cb) {
if (!(event in this.events)) {
this.events[event] = new Set([cb]);
} else {
this.events[event].add(cb);
}
return {
unsubscribe: () => {
this.events[event].delete(cb);
},
};
}
emit(event, args = []) {
if (!(event in this.events)) return [];
const result = [];
this.events[event].forEach((fn) => {
result.push(fn(...args));
});
return result;
}
}
复杂度分析:
- 时间复杂度:对于
subscribe和unsubscribe:O(1),对于emit:O(n),其中 n 表示已订阅事件的回调数。 - 空间复杂度:O(n),其中 n 表示已订阅事件的回调数。
面试提示
- 如何使用
EventEmitter类处理带参数的事件?- 当订阅事件时,回调函数可以接受事件参数作为参数。在触发事件时,可以向 emit 方法传递参数数组,它们将传递给已订阅的回调函数。回调可以在其实现中访问和使用这些参数。
- 是否可以订阅多个回调函数到同一事件使用
EventEmitter类?- 是的,
EventEmitter类允许多个回调函数订阅同一事件。每个订阅的回调将在事件触发时按照它们的订阅顺序被调用。
- 是的,
- 如何确保
EventEmitter类中回调执行的顺序?EventEmitter类通过按照它们被订阅的顺序存储回调函数来维护回调执行的顺序。在触发事件时,类会按照它们的订阅顺序遍历回调函数列表并调用每个回调函数。
- 当使用
EventEmitter类触发没有订阅的回调时会发生什么?- 如果没有回调订阅特定事件,使用
EventEmitter类触发该事件将返回一个空数组。这表示未执行任何回调,因为没有监听器订阅该事件。
- 如果没有回调订阅特定事件,使用
- 能否解释
EventEmitter与简单回调函数之间的区别?- 虽然简单的回调函数允许在事件发生时执行单个函数,但
EventEmitter提供了一种更有结构和可扩展性的方法来管理事件。通过EventEmitter,你可以为同一事件有多个回调,处理订阅和取消订阅,控制回调执行顺序,传递参数给回调,并获得更松散的架构。
- 虽然简单的回调函数允许在事件发生时执行单个函数,但