使用TypeScript实现一个类型安全的EventBus示例详解

作者:沉舟时间:2026-10-25 13:57:00浏览:380

本文通过TypeScript实现一个具备类型安全特性的EventBus,涵盖注册、发布与取消订阅核心功能。利用泛型约束确保事件名与参数类型匹配,结合函数重载优化IDE提示,提供完整代码示例与测试用例,帮助开发者构建类型安全的消息通信机制。

前言

伴随vue3的推出,TypeScript在国内热度日增,掌握TypeScript已成必然趋势。本文旨在通过构建一个类型安全的EventBus来实践TypeScript,期望能为读者提供借鉴。

准备工作

构建TypeScript项目的初始结构:

// 创建目录
mkdir ts-event-bus && cd ts-event-bus
// 初始化工程
yarn init -y
// 安装typescript
yarn add typescript -D
// 生成typescript配置文件
npx tsc --init

至此,TypeScript的基础框架已搭建完毕。为便于后续测试,需安装ts-node,该工具允许我们在不执行TypeScript编译步骤的情况下直接运行TypeScript脚本。

yarn add ts-node -D

目标

  • 实现核心功能,涵盖注册、发布及取消订阅三项基础操作。
  • 确保类型安全,对输入参数进行严格约束,并提供代码智能提示。

思路

每个Event可关联多个处理函数。我们利用Set存储这些函数,并通过Map维护Event与对应Set之间的映射关系,具体结构如图所示:

前言

具体实现

// 定义泛型函数类型
type Handler<T = any> = (val: T) => void;
class EventBus<Events extends Record<string, any>> {
  /** 保存 key => set 映射 */
  private map: Map<string, Set<Handler>> = new Map();
  on<EventName extends keyof Events>(
    name: EventName,
    handler: Handler<Events[EventName]>
  ) {
    let set: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!set) {
      set = new Set();
      this.map.set(name as string, set);
    }
    set.add(handler);
  }
}

此处将从逻辑实现与类型定义两个维度展开阐述。

在逻辑层面,我们初始化了一个空的Map。当调用on进行事件注册时,系统首先依据EventName查询是否存在对应的Set;若不存在则新建,并将事件处理函数加入Set。此部分逻辑简洁,易于实现。

在类型层面,我们将EventBus定义为泛型类,并限制泛型参数为Events extends Record<string, any>。此举确保了传入的泛型参数必须为对象类型,例如:

type Events = {
    foo : number;
    bar : string;
}

借助此类型定义,我们可以推导key所对应的value类型。

// number;
type ValueTypeOfFoo = Events['foo']

进而,能够获取foo事件所绑定的handler函数类型,即:

// (val:number) => void;
type HandlerOfFoo = Handler<Events['foo']>

我们将on方法定义为泛型函数,并对EventName extends keyof Events施加约束。由此,Events[EventName]对应具体的值类型,而Handler<Events[EventName]>对应处理函数类型。通过上述机制,我们成功构建了一个类型安全的on方法。

随后,我们编写测试代码进行验证。

结果显示,在VSCode中编写代码时,编辑器能够自动提供代码提示。

前言

当我们输入handler函数时,编辑器会提示val参数属于string类型。

前言

若传入的参数不符合规范,TypeScript编译器将发出警告。

前言

前言

接下来,我们参照类似逻辑实现emit函数。

class EventBus<Events extends Record<string, any>> {
 ... others code
  /** 触发事件 */
  emit<EventName extends keyof Events>(
    name: EventName,
    value: Events[EventName]
  ) {
    const set: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!set) return;
    const copied = [...set];
    copied.forEach((fn) => fn(value));
  }
}

首先定位EventName对应的Set,若存在则取出并依次执行。该逻辑同样简单,我们通过代码测试验证其正确性:

const bus = new EventBus<{
  foo: string;
  bar: number;
}>();

bus.on("foo", (val) => {
  console.log(val);
});

// 输出 hello
bus.emit("foo", "hello");

在终端执行npx ts-node ./index.ts后,输出“hello”,表明程序运行正常。

随后,我们实现取消订阅功能。

{
  ...
  off<EventName extends keyof Events>(
    name?: EventName,
    handler?: Handler<Events[EventName]>
  ): void {
    // 什么都不传,则清除所有事件
    if (!name) {
      this.map.clear();
      return;
    }

    // 只传名字,则清除同名事件
    if (!handler) {
      this.map.delete(name as string);
      return;
    }

    // name 和 handler 都传了,则清除指定handler
    const handlers: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!handlers) {
      return;
    }
    handlers.delete(handler);
  }
}

取消订阅功能设计为接受0至2个参数:不传参表示清除所有事件;仅传一个参数表示清除同名下的所有事件;传两个参数表示仅移除该事件指定的特定处理函数。因此,这两个参数均为可选参数,实现逻辑较为直观,此处不再赘述。

我们编写测试代码以观察实际效果。

const bus = new EventBus<{
  foo: string;
  bar: number;
}>();

// 测试传2个参数的情况
const handlerFoo1 = (val: string) => {
  console.log("2个参数 handlerFoo1 => ", val);
};
bus.on("foo", handlerFoo1);
bus.emit("foo", "hello");
// 打印 2个参数 handlerFoo1 => hello
bus.off("foo", handlerFoo1);
bus.emit("foo", "hello");
// 什么都没打印

// 测试传1个参数的情况
const handlerFoo2 = (val: string) => {
  console.log("1个参数 handlerFoo2 => ", val);
};
const handlerFoo3 = (val: string) => {
  console.log("1个参数 handlerFoo3 => ", val);
};
bus.on("foo", handlerFoo2);
bus.on("foo", handlerFoo3);

bus.emit("foo", "hello");
// 打印 1个参数 handlerFoo2 => hello
// 打印 1个参数 handlerFoo3 => hello
bus.off("foo");
bus.emit("foo", "hello");
// 什么都没输出

// 测试传0个参数的情况
const handlerFoo4 = (val: string) => {
  console.log("0个参数 handlerFoo4 => ", val);
};
const handlerBar1 = (val: number) => {
  console.log("0个参数 handlerBar1 => ", val);
};
bus.on("foo", handlerFoo4);
bus.on("bar", handlerBar1);

bus.emit("foo", "hello");
bus.emit("bar", 123);
// 打印 1个参数 handlerFoo4 => hello
// 打印 1个参数 handlerBar1 => 123
bus.off();
bus.emit("foo", "hello");
bus.emit("bar", 123);
// 什么都没输出

测试结果证实,我们的off方法功能正常,至此完成了EventBus的全部开发。

此外,我们可以为方法添加注释,这样在鼠标悬停于API上方或输入参数时,编辑器会显示相应的提示信息。

  /**
   * 订阅事件
   * @param name 事件名
   * @param handler 事件处理函数
   */
  on<EventName extends keyof Events>(
    name: EventName,
    handler: Handler<Events[EventName]>
  ) {
    let set: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!set) {
      set = new Set();
      this.map.set(name as string, set);
    }
    set.add(handler);
  }

前言

前言

可见,编辑器提供了极佳的提示体验,极大地提升了编码效率。

我们还可以利用函数重载技术优化off方法,以提供更友好的交互提示。

{
  /**
   *  清除所有事件
   */
  off(): void;
  /**
   * 清除同名事件
   * @param name 事件名
   */
  off<EventName extends keyof Events>(name: EventName): void;
  /**
   * 清除指定事件
   * @param name 事件名
   * @param handler 事件处理函数
   */
  off<EventName extends keyof Events>(
    name: EventName,
    handler: Handler<Events[EventName]>
  ): void;
  off<EventName extends keyof Events>(
    name?: EventName,
    handler?: Handler<Events[EventName]>
  ): void {
    // 什么都不传,则清除所有事件
    if (!name) {
      this.map.clear();
      return;
    }

    // 只传名字,则清除同名事件
    if (!handler) {
      this.map.delete(name as string);
      return;
    }

    // name 和 handler 都传了,则清除指定handler
    const handlers: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!handlers) {
      return;
    }
    handlers.delete(handler);
  }
}

改造前的提示效果:

前言

改造后的提示效果:

前言

至此,一个功能完备且类型安全的EventBus便开发完成。

全部代码

type Handler<T = any> = (val: T) => void;

class EventBus<Events extends Record<string, any>> {
  private map: Map<string, Set<Handler>> = new Map();

  /**
   * 订阅事件
   * @param name 事件名
   * @param handler 事件处理函数
   */
  on<EventName extends keyof Events>(
    name: EventName,
    handler: Handler<Events[EventName]>
  ) {
    let set: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!set) {
      set = new Set();
      this.map.set(name as string, set);
    }
    set.add(handler);
  }

  /**
   * 触发事件
   * @param name 事件名
   * @param handler 事件处理函数
   */
  emit<EventName extends keyof Events>(
    name: EventName,
    value: Events[EventName]
  ) {
    const set: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!set) return;
    const copied = [...set];
    copied.forEach((fn) => fn(value));
  }
  /**
   *  清除所有事件
   */
  off(): void;
  /**
   * 清除同名事件
   * @param name 事件名
   */
  off<EventName extends keyof Events>(name: EventName): void;
  /**
   * 清除指定事件
   * @param name 事件名
   * @param handler 处理函数
   */
  off<EventName extends keyof Events>(
    name: EventName,
    handler: Handler<Events[EventName]>
  ): void;

  off<EventName extends keyof Events>(
    name?: EventName,
    handler?: Handler<Events[EventName]>
  ): void {
    // 什么都不传,则清除所有事件
    if (!name) {
      this.map.clear();
      return;
    }

    // 只传名字,则清除同名事件
    if (!handler) {
      this.map.delete(name as string);
      return;
    }

    // name 和 handler 都传了,则清除指定handler
    const handlers: Set<Handler<Events[EventName]>> | undefined = this.map.get(
      name as string
    );
    if (!handlers) {
      return;
    }
    handlers.delete(handler);
  }
}

const bus = new EventBus<{
  foo: string;
  bar: number;
}>();

// 测试传2个参数的情况
const handlerFoo1 = (val: string) => {
  console.log("2个参数 handlerFoo1 => ", val);
};
bus.on("foo", handlerFoo1);
bus.emit("foo", "hello");
// 打印 2个参数 handlerFoo1 => hello
bus.off("foo", handlerFoo1);
bus.emit("foo", "hello");
// 什么都没打印

// 测试传1个参数的情况
const handlerFoo2 = (val: string) => {
  console.log("1个参数 handlerFoo2 => ", val);
};
const handlerFoo3 = (val: string) => {
  console.log("1个参数 handlerFoo3 => ", val);
};
bus.on("foo", handlerFoo2);
bus.on("foo", handlerFoo3);

bus.emit("foo", "hello");
// 打印 1个参数 handlerFoo2 => hello
// 打印 1个参数 handlerFoo3 => hello
bus.off("foo");
bus.emit("foo", "hello");
// 什么都没输出

// 测试传0个参数的情况
const handlerFoo4 = (val: string) => {
  console.log("0个参数 handlerFoo4 => ", val);
};
const handlerBar1 = (val: number) => {
  console.log("0个参数 handlerBar1 => ", val);
};
bus.on("foo", handlerFoo4);
bus.on("bar", handlerBar1);

bus.emit("foo", "hello");
bus.emit("bar", 123);
// 打印 1个参数 handlerFoo4 => hello
// 打印 1个参数 handlerBar1 => 123
bus.off();
bus.emit("foo", "hello");
bus.emit("bar", 123);
// 什么都没输出

后记

EventBus是日常开发中的常用工具。本文利用Typescript实现了一个具备基础功能且类型安全的EventBus,这是笔者近期学习Typescript知识的总结,希望能对各位有所助益。

本文相关代码已同步至GitHub,感兴趣的同学可以clone源码进行学习。若觉得有用,欢迎点赞支持!

关于使用TypeScript实现类型安全EventBus的文章介绍至此,更多TypeScript实现类型安全EventBus相关内容,请查阅本站过往文章或继续浏览后续文章。希望大家今后能多多支持本站!

作者最新文章
相关文章
更多