发布订阅模式 - EventBus

122 阅读7分钟

首先,我应该从整体结构入手。类名为EventBus,包含一个私有属性events,用来存储事件名和对应的处理函数数组。接下来有四个方法:on、emit、off和once。我需要逐一解释每个方法的作用和实现细节。

在解释on方法时,要注意事件名的类型是字符串,handler是事件处理函数。当订阅事件时,如果该事件不存在,就初始化一个空数组,然后将处理函数添加到数组中。这部分需要说明如何处理重复订阅,以及如何存储多个处理函数。

接下来是emit方法,触发指定事件时,会遍历该事件的所有处理函数并执行,同时传递参数。这里需要注意参数是如何通过展开运算符传递的,以及如何处理不存在的事件名,避免运行时错误。

off方法用于取消订阅,需要传入事件名和处理函数。这里使用filter方法过滤掉要移除的处理函数,并重新赋值给原事件数组。需要说明如果没有找到对应的处理函数会发生什么,以及如何处理不存在的事件名。

once方法实现一次性订阅,内部创建一个onceHandler,当执行后会立即移除自身。这里的关键在于onceHandler如何包装原始处理函数,并在调用后调用off方法移除自己。

然后,我需要考虑类型定义,比如EventHandler的类型是接收任意参数且无返回的函数。TypeScript的使用增强了类型安全,避免了一些潜在的错误。

接下来要讨论代码的优点和可能的改进点。比如,没有处理重复订阅的问题,同一个处理函数可能被多次添加,导致多次触发。此外,异步执行或错误处理也没有涉及,可能在实际应用中需要增强。

还要考虑实际使用场景,比如在前端框架中的组件通信,或者跨模块的事件传递。示例使用情况可以帮助用户更好地理解如何应用这个EventBus类。

可能还需要对比之前用户提供的代码,说明优化后的版本在类型安全、方法命名和健壮性上的改进。例如,之前代码中的类型不明确和参数传递错误在这里得到了修正。

最后,总结这段代码的核心功能和适用场景,强调发布-订阅模式的优势,如解耦和扩展性,同时指出可能的注意事项,比如内存泄漏和错误处理。

以下是对这段 EventBus 代码的逐行解析,从核心原理到实现细节的完整说明:


一、代码结构概览

整体代码实现

type EventHandler = (...args: any[]) => void;

class EventBus {
  private events: { [eventName: string]: EventHandler[] } = {};

  // 订阅事件
  on(eventName: string, handler: EventHandler): void {
    if (!this.events[eventName]) {
      this.events[eventName] = [];
    }
    this.events[eventName].push(handler);
  }

  // 发布事件
  emit(eventName: string, ...args: any[]): void {
    const handlers = this.events[eventName];
    if (handlers) {
      handlers.forEach(handler => handler(...args));
    }
  }

  // 取消订阅
  off(eventName: string, handler: EventHandler): void {
    const handlers = this.events[eventName];
    if (handlers) {
      this.events[eventName] = handlers.filter(h => h !== handler);
    }
  }

  // 一次性订阅
  once(eventName: string, handler: EventHandler): void {
    const onceHandler: EventHandler = (...args) => {
      handler(...args);
      this.off(eventName, onceHandler);
    };
    this.on(eventName, onceHandler);
  }
}
  • 核心目标:实现一个轻量级的发布-订阅模式工具,支持事件监听、触发、取消监听和一次性监听。

  • 关键设计

    • 使用 events 对象存储事件与处理函数的映射。
    • 类型安全:通过 EventHandler 类型明确定义回调函数格式。

二、方法详解

1. on 方法(订阅事件)

on(eventName: string, handler: EventHandler): void {
  if (!this.events[eventName]) {
    this.events[eventName] = [];
  }
  this.events[eventName].push(handler);
}
  • 作用:为指定事件注册回调函数。

  • 流程

    1. 检查 eventName 是否存在,若不存在则初始化为空数组。
    2. 将 handler 添加到该事件的回调列表中。
  • 特性

    • 允许多次订阅同一事件,回调按注册顺序执行。
    • 无去重检查:同一函数可被重复添加,可能造成多次触发。

2. emit 方法(触发事件)

emit(eventName: string, ...args: any[]): void {
  const handlers = this.events[eventName];
  if (handlers) {
    handlers.forEach(handler => handler(...args));
  }
}
  • 作用:触发指定事件,执行所有关联的回调函数。

  • 流程

    1. 获取 eventName 对应的回调列表。
    2. 若列表存在,遍历并逐个调用回调函数,传入 args 参数。
  • 特性

    • 参数通过展开运算符传递,支持任意数量和类型的参数。
    • 同步执行:所有回调函数按注册顺序同步执行。

3. off 方法(取消订阅)

off(eventName: string, handler: EventHandler): void {
  const handlers = this.events[eventName];
  if (handlers) {
    this.events[eventName] = handlers.filter(h => h !== handler);
  }
}
  • 作用:移除指定事件的某个回调函数。

  • 流程

    1. 获取 eventName 的回调列表。
    2. 使用 filter 过滤掉与 handler 相同的回调,更新列表。
  • 特性

    • 严格匹配函数引用,匿名函数无法取消。
    • 若事件不存在或无匹配回调,静默失败。

4. once 方法(一次性订阅)

once(eventName: string, handler: EventHandler): void {
  const onceHandler: EventHandler = (...args) => {
    handler(...args);
    this.off(eventName, onceHandler);
  };
  this.on(eventName, onceHandler);
}
  • 作用:注册一个只执行一次的回调函数。

  • 流程

    1. 创建包装函数 onceHandler,在执行原始 handler 后调用 off 移除自身。
    2. 将 onceHandler 注册到 eventName
  • 特性

    • 利用闭包保存对原始 handler 和 eventName 的引用。
    • 实际存储的是 onceHandler,而非原始 handler

三、关键设计解析

1. 数据结构设计

private events: { [eventName: string]: EventHandler[] } = {};
  • 哈希表结构:以 eventName 为键,值为回调函数数组。
  • 优势:O(1) 时间复杂度的事件查找,O(n) 的回调执行(n 为回调数量)。

2. 类型安全

type EventHandler = (...args: any[]) => void;
  • 定义回调类型:明确函数参数为任意类型数组,无返回值。
  • 避免类型错误:强制 onoff 等方法使用一致的回调格式。

3. 一次订阅的实现

const onceHandler: EventHandler = (...args) => {
  handler(...args);      // 执行原始回调
  this.off(eventName, onceHandler); // 移除自身
};
  • 闭包应用onceHandler 捕获 handler 和 eventName,确保移除正确的回调。
  • 自移除逻辑:回调执行后立即解除订阅,避免重复触发。

四、使用示例

1. 基本使用

const bus = new EventBus();

// 订阅事件
bus.on('message', (text: string) => {
  console.log(`收到消息:${text}`);
});

// 触发事件
bus.emit('message', 'Hello World'); // 输出:收到消息:Hello World

// 取消订阅
const handler = (data: number) => console.log(`数据:${data}`);
bus.on('data', handler);
bus.off('data', handler);
bus.emit('data', 100); // 无输出

// 一次性订阅
bus.once('alert', (msg: string) => {
  console.log(`警告:${msg}`);
});
bus.emit('alert', '系统故障'); // 输出:警告:系统故障
bus.emit('alert', '再次触发'); // 无输出

2. 多订阅场景

bus.on('update', () => console.log('回调1'));
bus.on('update', () => console.log('回调2'));
bus.emit('update'); 
// 输出:
// 回调1
// 回调2

五、潜在问题与改进

1. 问题总结

问题影响改进方案
重复添加回调同一函数被多次执行在 on 中添加存在性检查
匿名函数无法取消导致内存泄漏要求使用具名函数,或提供唯一标识符
无错误处理某个回调报错会导致后续回调中断用 try...catch 包裹回调执行
异步支持不足无法处理异步回调顺序提供 emitAsync 方法,返回 Promise

2. 改进示例:防重复订阅

on(eventName: string, handler: EventHandler): void {
  if (!this.events[eventName]) {
    this.events[eventName] = [];
  }
  // 防止重复添加
  if (!this.events[eventName].includes(handler)) {
    this.events[eventName].push(handler);
  }
}

3. 改进示例:异步触发

async emitAsync(eventName: string, ...args: any[]): Promise<void> {
  const handlers = this.events[eventName];
  if (handlers) {
    for (const handler of handlers) {
      await handler(...args); // 支持异步回调顺序执行
    }
  }
}

六、适用场景

  1. 跨组件通信:Vue/React 中非父子组件的消息传递。
  2. 插件系统:通过事件扩展功能(如编辑器插件触发保存事件)。
  3. 状态变更通知:数据更新后通知多个监听模块。
  4. 解耦复杂逻辑:将业务逻辑拆分为独立的事件处理器。

总结

此 EventBus 实现了一个典型的发布-订阅模型,核心在于通过事件中心解耦消息发布者与订阅者。其简洁性适合中小规模应用,但在生产环境中需根据实际需求扩展错误处理、异步支持和防重复机制。理解此代码有助于掌握事件驱动架构的基本原理,并为进一步优化提供基础。