EventEmitter

基础知识

EventEmitter,即事件发射器,是一个实现了“发布-订阅”设计模式的类。这个模式允许我们定义一种一对多的依赖关系,当一个对象(发布者)的状态发生改变时,所有依赖于它的对象(订阅者)都会得到通知并自动更新。

  1. 发布者 (Publisher): 也称为“事件发射器”,是事件的中心枢纽。它负责维护事件列表,并在适当时机“发布”或“发射”事件。

  2. 订阅者 (Subscriber): 也称为“事件监听器”(Listener),它是一个函数。它向发布者“订阅”自己感兴趣的特定事件。

  3. 主题/事件 (Topic/Event): 一个用于区分不同消息类型的标签,通常是一个字符串(如 'click', 'data', 'error')。订阅者订阅的是特定的事件,发布者发布的也是特定的事件。

EventEmitter 的核心 API 包括:

  • on(eventName, listener): 订阅一个事件。
  • emit(eventName, ...args): 发布一个事件。
  • off(eventName, listener): 取消订阅一个事件。
  • once(eventName, listener): 订阅一个只执行一次的事件。

核心思路

我们的目标是创建一个 EventEmitter 类。其核心思路是内部维护一个“事件中心”,用于存储所有事件和其对应的监听器。

  1. 事件中心的数据结构: 最合适的结构是一个字典(或 Map),其中:

    • 键 (key) 是事件名称 (eventName) 的字符串。
    • 值 (value) 是一个数组,包含了所有订阅了该事件的监听器函数(listener)。
    // 伪代码
    this._events = {
      event1: [listenerA, listenerB],
      event2: [listenerC],
    };
    
  2. on(eventName, listener) 的逻辑:

    • 检查事件中心里是否已经有 eventName 这个键。
    • 如果没有,就创建一个新数组作为值:this._events[eventName] = []
    • listener 函数推入(push)到这个数组中。
  3. emit(eventName, ...args) 的逻辑:

    • 查找事件中心里 eventName 对应的监听器数组。
    • 如果找到了,就遍历这个数组,并依次执行其中的每一个监听器函数,同时将 ...args 参数传递给它们。
  4. off(eventName, listener) 的逻辑:

    • 查找 eventName 对应的监听器数组。
    • 如果找到了,就从数组中找到并移除指定的 listener 函数。
  5. once(eventName, listener) 的逻辑:

    • 这是一个巧妙的封装。我们可以创建一个临时的“包装函数”。
    • 在这个包装函数内部,首先执行原始的 listener,然后立即调用 off 方法将这个包装函数自身从事件中移除。
    • 最后,使用 on 方法订阅这个包装函数。

代码实现

下面是一个包含核心功能的 EventEmitter 类的完整实现。

/**
 * 实现一个 EventEmitter 类
 */
class EventEmitter {
  constructor() {
    // 1. 初始化事件中心,用于存储事件和监听器
    this._events = {};
  }

  /**
   * 订阅事件
   * @param {string} eventName - 事件名称
   * @param {Function} listener - 监听器函数
   */
  on(eventName, listener) {
    if (!this._events[eventName]) {
      this._events[eventName] = [];
    }
    this._events[eventName].push(listener);
    return this; // 支持链式调用
  }

  /**
   * 发布事件
   * @param {string} eventName - 事件名称
   * @param {...any} args - 传递给监听器的参数
   */
  emit(eventName, ...args) {
    const listeners = this._events[eventName];
    if (!listeners || listeners.length === 0) {
      return false; // 如果没有监听器,则直接返回
    }

    // 2. 创建一个 listeners 数组的副本进行遍历,防止在 emit 过程中有 off 操作导致数组塌陷
    const listenersCopy = [...listeners];
    listenersCopy.forEach((listener) => {
      listener(...args);
    });

    return true;
  }

  /**
   * 取消订阅事件
   * @param {string} eventName - 事件名称
   * @param {Function} listener - 要移除的监听器函数
   */
  off(eventName, listener) {
    const listeners = this._events[eventName];
    if (!listeners || listeners.length === 0) {
      return this;
    }

    // 3. 使用 filter 过滤掉要移除的监听器
    this._events[eventName] = listeners.filter((l) => l !== listener);

    return this;
  }

  /**
   * 订阅一个只执行一次的事件
   * @param {string} eventName - 事件名称
   * @param {Function} listener - 监听器函数
   */
  once(eventName, listener) {
    // 4. 创建一个包装函数
    const onceWrapper = (...args) => {
      // 在执行原始监听器后,立即移除自身
      listener(...args);
      this.off(eventName, onceWrapper);
    };

    // 订阅这个包装函数
    this.on(eventName, onceWrapper);
    return this;
  }
}

代码解析

  1. constructor 初始化

    • 初始化 _events 属性为一个空对象,用于存储事件与监听器的映射关系。
    • 下划线前缀 _ 是约定俗成的内部属性标识,提示外部代码不应直接访问该属性。
  2. emit 中的副本遍历

    • emit 方法中,通过 const listenersCopy = [...listeners] 创建监听器数组的副本。
    • 若直接遍历原始数组,当某个监听器执行 off 操作移除同事件的其他监听器时,会导致遍历过程中数组结构改变,可能跳过部分监听器。
    • 遍历副本可确保本次 emit 触发时所有应调用的监听器都被正确执行,避免因数组动态修改引发的逻辑错误。
  3. off 中的 filter 过滤

    • 使用 Array.prototype.filter 方法移除指定监听器,是最简洁安全的实现方式。
    • 该方法会创建一个新数组,过滤掉与目标监听器引用相同的项,再用新数组覆盖原数组,完成监听器的移除操作,确保数组操作的原子性和准确性。
  4. once 中的 onceWrapper 包装函数

    • once 方法通过创建 onceWrapper 包装函数实现单次监听逻辑。
    • onceWrapper 被首次调用时,先执行原始监听器 listener 并传递参数,随后立即调用 this.off(eventName, onceWrapper) 从监听器数组中移除自身。
    • 这种“自销毁”机制确保原始监听器仅在事件首次触发时执行,后续相同事件触发时不再响应,无需额外状态管理,实现简洁且高效。
Copyright © Jun 2025 all right reserved,powered by Gitbook该文件修订时间: 2025-07-03 17:35:08

results matching ""

    No results matching ""

    results matching ""

      No results matching ""