You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

自定义Emitter类once方法实现求助:需返回订阅数并修复执行/移除异常

解决自定义Emitter类once方法的问题

我来帮你搞定这个once方法的问题!首先得明确咱们的核心需求:绑定的回调只会触发一次,触发后自动从订阅列表中移除,同时这个方法要返回指定事件当前的订阅者数量。你之前的问题大概率是没正确包装回调,导致触发时没法自动解绑,或者计数逻辑没处理好。

完整修正后的Emitter类代码

class Emitter {
  constructor() {
    // 用对象存储事件:键为事件名,值为回调函数数组
    this.events = {};
  }

  on(eventName, callBack) {
    // 校验事件名类型
    if (typeof eventName !== 'string') {
      throw new TypeError('事件名称必须是字符串类型');
    }
    // 初始化事件数组(如果不存在的话)
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    // 添加回调到订阅列表
    this.events[eventName].push(callBack);
  }

  // 修复后的once方法
  once(eventName, callBack) {
    // 先做基础类型校验
    if (typeof eventName !== 'string') {
      throw new TypeError('事件名称必须是字符串类型');
    }
    if (typeof callBack !== 'function') {
      throw new TypeError('回调参数必须是函数类型');
    }

    // 包装一个一次性回调:执行原回调后自动移除自己
    const onceCallback = (...args) => {
      // 先执行用户传入的原回调
      callBack.apply(this, args);
      // 执行完毕后,立刻从事件列表中移除这个包装后的回调
      this.off(eventName, onceCallback);
    };

    // 初始化事件数组(如果不存在的话)
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    // 将包装后的回调加入订阅列表
    this.events[eventName].push(onceCallback);

    // 返回当前该事件的订阅者总数
    return this.events[eventName].length;
  }

  // 补充off方法(用于移除回调,之前如果没实现的话必须加上)
  off(eventName, callBack) {
    if (typeof eventName !== 'string') {
      throw new TypeError('事件名称必须是字符串类型');
    }
    // 如果事件不存在,直接返回
    if (!this.events[eventName]) return;
    // 过滤掉要移除的回调
    this.events[eventName] = this.events[eventName].filter(cb => cb !== callBack);
    // 若订阅列表为空,删除该事件键以节省内存
    if (this.events[eventName].length === 0) {
      delete this.events[eventName];
    }
  }

  // 补充emit方法(触发事件,确保能正确执行回调)
  emit(eventName, ...args) {
    if (typeof eventName !== 'string') {
      throw new TypeError('事件名称必须是字符串类型');
    }
    // 如果事件不存在,直接返回
    if (!this.events[eventName]) return;
    // 复制回调数组再遍历,避免执行一次性回调时修改原数组导致遍历异常
    [...this.events[eventName]].forEach(callBack => {
      callBack.apply(this, args);
    });
  }
}

关键逻辑拆解

  • 包装一次性回调:不能直接把用户传入的回调加入订阅列表,必须用一个新函数包装它。这个包装函数会先执行原回调,然后立刻调用off把自己从列表中移除,完美实现“只触发一次”的需求。
  • 类型校验:和on方法保持一致的校验逻辑,避免非法输入导致的bug。
  • 返回订阅数:添加完包装后的回调后,直接返回当前事件列表的长度,满足你要求的返回值需求。
  • emit时的数组复制:用[...this.events[eventName]]复制一份回调数组再遍历,防止执行一次性回调时,因off操作修改原数组而导致的遍历遗漏或错误。

测试示例

// 实例化Emitter
const emitter = new Emitter();

// 绑定once事件,返回当前订阅数1
const subscriberCount = emitter.once('testEvent', () => console.log('我只会被触发一次!'));
console.log(subscriberCount); // 输出:1

// 第一次触发事件,会打印内容,之后回调被自动移除
emitter.emit('testEvent'); // 输出:"我只会被触发一次!"

// 第二次触发事件,不会有任何输出(回调已被移除)
emitter.emit('testEvent');

// 查看当前testEvent的订阅状态,应该是undefined(因为列表为空被删除了)
console.log(emitter.events.testEvent); // 输出:undefined

内容的提问来源于stack exchange,提问作者Sahin Erbay

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 04:18:10