手写发布订阅(Event Emitter):实现 `on`、`emit`、`off` 和 `once` 方法

手写发布订阅模式:从零实现 Event Emitter 的完整指南

大家好,欢迎来到今天的讲座。今天我们不聊框架、不谈算法,而是深入到 JavaScript 底层机制中,一起手写一个经典的 Event Emitter(事件发射器) 实现。它虽然看似简单,但却是 Node.js 核心模块、前端状态管理库(如 Redux、Vuex)、以及各类异步通信系统的基础。

你可能在项目中用过 eventEmitter.on('click', handler) 这样的代码,但你知道它是怎么工作的吗?我们今天的目标就是——亲手实现一套完整的 onemitoffonce 方法,让你真正理解“发布-订阅”模式的本质。


一、什么是发布订阅模式?

发布订阅是一种行为设计模式,允许对象之间解耦地通信。它有两个角色:

角色 职责
发布者(Publisher) 发送事件(emit),通知所有监听者
订阅者(Subscriber) 监听特定事件(on),执行回调函数

这种模式的好处是:

  • 解耦:发布者不需要知道谁在监听
  • 灵活扩展:可以动态添加或移除监听器
  • 支持多对多通信:一个事件可被多个监听者处理

Node.js 中的 EventEmitter 就是一个经典例子。我们的目标就是复刻它的核心功能。


二、基础结构设计

首先定义一个类 EventEmitter,内部维护一个事件映射表(Map 或 Object),用来存储每个事件对应的监听器数组。

class EventEmitter {
  constructor() {
    this.events = {};
  }

  // 后续方法将在这里实现
}

✅ 基础数据结构说明:

属性 类型 描述
events Object<string, Array<Function>> 键为事件名,值为该事件的所有监听器数组

为什么用数组而不是 Set?
因为我们需要支持重复注册同一个回调(比如多次调用 on('foo', fn)),而且要能按顺序执行(FIFO)。Set 不适合记录重复项,而数组天然有序且可重复。


三、实现 on(event, listener) —— 添加监听器

这是最基础的方法,用于注册某个事件的监听器。

on(event, listener) {
  if (!this.events[event]) {
    this.events[event] = [];
  }
  this.events[event].push(listener);
  return this; // 支持链式调用
}

💡 注意点:

  • 如果事件不存在,创建空数组;
  • 使用 push 添加监听器;
  • 返回 this 是为了支持链式调用,例如:emitter.on('a', fn).on('b', fn2)

测试一下:

const emitter = new EventEmitter();

emitter.on('hello', () => console.log('Hello!'));
emitter.on('hello', () => console.log('Hi there!'));

emitter.emit('hello'); 
// 输出:
// Hello!
// Hi there!

✅ 成功!两个监听器都被触发了。


四、实现 emit(event, ...args) —— 触发事件

这个方法负责遍历指定事件的所有监听器并依次执行它们,同时传入参数。

emit(event, ...args) {
  const listeners = this.events[event];
  if (!listeners || listeners.length === 0) {
    return false; // 没有监听器返回 false 表示未处理
  }

  listeners.forEach(listener => {
    try {
      listener.apply(this, args); // 保证 this 指向 emitter
    } catch (err) {
      console.error(`Listener error for event "${event}":`, err);
    }
  });

  return true;
}

🔍 关键细节:

  • 若无监听器直接返回 false,避免无效操作;
  • 使用 forEach 遍历,确保顺序性;
  • 包裹 try-catch,防止单个监听器出错影响整体流程;
  • apply(this, args) 保持上下文一致性,即 this 在监听器内指向 EventEmitter 实例。

测试:

emitter.emit('hello', 'world');
// 输出:
// Hello!
// Hi there!

完美匹配预期!


五、实现 off(event, listener) —— 移除监听器

删除某个事件下的指定监听器,注意不能删错。

off(event, listener) {
  const listeners = this.events[event];
  if (!listeners) return this;

  const index = listeners.indexOf(listener);
  if (index !== -1) {
    listeners.splice(index, 1);
  }

  // 如果列表为空,可选地清理 key(非必须)
  if (listeners.length === 0) {
    delete this.events[event];
  }

  return this;
}

⚠️ 注意事项:

  • 必须先判断是否存在该事件;
  • 使用 indexOf 找到索引,然后 splice 删除;
  • 删除后如果数组为空,可以主动删除 key(可选优化);
  • 返回 this 支持链式调用。

测试:

const fn1 = () => console.log('fn1');
const fn2 = () => console.log('fn2');

emitter.on('test', fn1);
emitter.on('test', fn2);

emitter.off('test', fn1); // 删除 fn1
emitter.emit('test'); // 只输出 fn2

✅ 正确移除指定监听器!


六、实现 once(event, listener) —— 仅执行一次的监听器

这是高级特性,常用于一次性任务,比如加载完成、连接建立等场景。

思路:包装原始监听器,在第一次执行后自动移除自己。

once(event, listener) {
  const onceWrapper = (...args) => {
    this.off(event, onceWrapper); // 自动移除自身
    listener.apply(this, args);   // 执行原监听器
  };

  this.on(event, onceWrapper);
  return this;
}

📌 核心逻辑:

  • 创建一个新的包装函数 onceWrapper
  • 它会先调用 off 移除自己,再执行原始监听器;
  • 然后把这个 wrapper 注册为监听器;
  • 这样就实现了“只运行一次”的效果。

测试:

let count = 0;
const handler = () => {
  count++;
  console.log(`Called ${count} times`);
};

emitter.once('single', handler);
emitter.emit('single'); // 输出: Called 1 times
emitter.emit('single'); // 无输出(已移除)
emitter.emit('single'); // 无输出

✅ 完美!只执行了一次。


七、增强版:支持通配符和命名空间(进阶)

虽然不是必须,但我们也可以扩展一些实用功能,比如:

  • 通配符监听:如 * 匹配所有事件;
  • 命名空间:如 'user.login''user.logout',便于组织事件。

但这部分属于进阶内容,建议先掌握基础版本后再考虑添加。

🧠 提示:如果你正在开发大型应用,这类能力非常有用,比如 React 的 Context API 或 Vue 的 EventBus 都会用到类似机制。


八、完整代码整合与使用示例

下面是最终的完整实现:

class EventEmitter {
  constructor() {
    this.events = {};
  }

  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    return this;
  }

  emit(event, ...args) {
    const listeners = this.events[event];
    if (!listeners || listeners.length === 0) {
      return false;
    }

    listeners.forEach(listener => {
      try {
        listener.apply(this, args);
      } catch (err) {
        console.error(`Listener error for event "${event}":`, err);
      }
    });

    return true;
  }

  off(event, listener) {
    const listeners = this.events[event];
    if (!listeners) return this;

    const index = listeners.indexOf(listener);
    if (index !== -1) {
      listeners.splice(index, 1);
    }

    if (listeners.length === 0) {
      delete this.events[event];
    }

    return this;
  }

  once(event, listener) {
    const onceWrapper = (...args) => {
      this.off(event, onceWrapper);
      listener.apply(this, args);
    };

    this.on(event, onceWrapper);
    return this;
  }
}

// 使用示例
const emitter = new EventEmitter();

emitter.on('greet', name => console.log(`Hello, ${name}!`));
emitter.once('once', () => console.log('This runs only once.'));
emitter.on('greet', name => console.log(`Welcome, ${name}!`));

emitter.emit('greet', 'Alice'); // Hello, Alice! Welcome, Alice!
emitter.emit('once');          // This runs only once.
emitter.emit('once');          // No output

九、常见问题与陷阱解析

问题 解释 如何解决
内存泄漏? 如果忘记 off,可能导致监听器无法释放 推荐配合 once 或手动管理生命周期
错误传播? 单个监听器报错导致整个事件中断 使用 try-catch 包裹每个监听器
this 上下文混乱? 监听器中的 this 不等于 EventEmitter 使用 .apply(this, args) 显式绑定
性能瓶颈? 大量监听器时遍历开销大 对于高频事件,可考虑分组或限制最大数量
重复注册? 多次调用 on 导致冗余监听器 可加去重逻辑(如用 Set 存储监听器引用)

✅ 总结:这些都不是致命缺陷,而是需要开发者根据业务场景权衡取舍的问题。


十、实际应用场景举例

  1. 前端状态管理

    const store = new EventEmitter();
    store.on('stateChange', newState => updateUI(newState));
  2. WebSocket 通信封装

    class Socket extends EventEmitter {
      connect() {
        this.emit('connect');
      }
      onData(data) {
        this.emit('data', data);
      }
    }
  3. 插件架构

    const pluginManager = new EventEmitter();
    pluginManager.on('pluginLoaded', plugin => registerPlugin(plugin));
  4. 日志系统

    const logger = new EventEmitter();
    logger.once('start', () => log('App started'));

你会发现,几乎任何需要“事件驱动”的地方都可以用它来简化设计。


十一、总结与升华

今天我们从头开始构建了一个完整的 Event Emitter,涵盖了四个核心方法:

方法 功能 是否推荐
on 注册监听器 ✅ 必备
emit 触发事件 ✅ 必备
off 移除监听器 ✅ 必备
once 一次性监听器 ✅ 强烈推荐

通过这次实践,你应该已经掌握了:

  • 事件系统的底层原理;
  • 如何优雅地处理异常;
  • 如何让代码更健壮、易维护;
  • 以及如何将其融入真实项目中。

这不是一个简单的“玩具代码”,而是你在未来工作中随时可能用到的核心技能之一。

📌 最后一句忠告:

“不要害怕复杂的设计,要学会拆解问题。”
发布订阅模式看似简单,但它背后体现的是解耦思想事件驱动编程范式——这正是现代软件工程的灵魂所在。

希望今天的讲解对你有所启发,也欢迎你在评论区分享你的理解和改进方案!我们下次再见!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注