【力扣】2694. 事件发射器

【力扣】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 <= 10
  • values.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)并将提供的参数传递给它。
      • 最后,将每个回调函数调用的结果收集并返回为数组。
实现:
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,你可以为同一事件有多个回调,处理订阅和取消订阅,控制回调执行顺序,传递参数给回调,并获得更松散的架构。
相关推荐
JavaGuide几秒前
NVIDIA 又开源了!这次给 AI Agent 加上权限管控
前端·后端
excel10 分钟前
prisma 如何处理数据库竞态
前端·数据库·后端
Cccp.1231 小时前
【leetcode】(七)前缀树和贪心算法
数据结构·leetcode·贪心算法
天真小巫1 小时前
2026.9.28总结
职场和发展
千里码aicood1 小时前
基于CART算法的图书分类系统设计与实现
数据库·算法·分类
我命由我123451 小时前
经纪人的职业定义
经验分享·笔记·学习·职场和发展·求职招聘·职场发展·学习方法
做cv的小昊1 小时前
【World Model】π0.5:a Vision-Language-Action Model with Open-World Generalization
人工智能·算法·机器学习·大模型·多模态·vla·世界模型
莪_幻尘1 小时前
Skill 体检:30 个 Skill 全凭感觉?体检器先自曝了 8 个“假 0 分
前端·人工智能·llm
风骏时光牛马2 小时前
AI模型综合能力评测:性能、指令遵循与多场景实测对比
前端