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

目录
  • 前言
  • 准备工作
  • 目标
  • 思路
  • 具体实现
  • 全部代码
  • 后记

前言

随着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内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • TypeScript 映射类型详情

    目录 1.映射类型(Mapped Types) 2.映射修饰符(Mapping Modifiers) 3.通过 as 实现键名重新映射(Key Remapping via as) 4.深入探索(Further Exploration) 前言: TypeScript 的官方文档早已更新,但我能找到的中文文档都还停留在比较老的版本.所以对其中新增以及修订较多的一些章节进行了翻译整理. 本篇翻译整理自 TypeScript Handbook 中 「Mapped Types」 章节. 本文并不严格按照原

  • 使用TypeScript类型注解的方法

    目录 类型注解 类型推导 TS和JS共有的数据类型 TS独有的数据类型 any unknown void never tuple 函数参数和返回值 类型断言 非空类型断言 字面量 类型缩小 总结 类型注解 TypeScript提供了很多数据类型,通过类型对变量进行限制,称之为类型注解,使用类型注解后,就不能够随意变更变量的类型. 以下代码定义了一个字符串类型的变量,如果把它更改为数字类型时,代码编译阶段就会直接报错,提示 “Type ‘number’ is not assignable to t

  • TypeScript合并两个排序链表的方法详解

    目录 前言 思路分析 实现代码 测试用例 示例代码 前言 给定两个递增排序的链表,如何将这两个链表合并?合并后的链表依然按照递增排序.本文就跟大家分享一种解决方案,欢迎各位感兴趣的开发者阅读本文. 思路分析 经过前面的学习,我们知道了有关链表的操作可以用指针来完成.同样的,这个问题也可以用双指针的思路来实现: p1指针指向链表1的头节点 p2指针指向链表2的头节点 声明一个变量存储合并后的链表,比对两个指针指向的节点值大小: 如果p1指针指向的节点值比p2指向的值小,合并后的链表节点就取p1节点

  • 一起来学习TypeScript的类型

    目录 前言 一.类型声明 二.类型 1.number 2.string 3.boolean 4.字面量 5.联合类型 6.any 7.unknown 8.void 9.never 10.object 11.array 12.tuple 13.枚举enum 14.其他 总结 前言 TypeScript学习笔记第一部分,关于TS的类型声明以及基本类型. 一.类型声明 类型声明 类型声明是TS非常重要的一个特点 通过类型声明可以指定TS中变量(参数.形参)的类型 指定类型后,当为变量赋值时,TS编译器

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

    目录 前言 准备工作 目标 思路 具体实现 全部代码 后记 前言 随着vue3的发布,TypeScript在国内越来越流行,学习TypeScript也随即变成了大势所趋.本文就通过实现一个类型安全的EventBus来练习TypeScript,希望对小伙伴们有所帮助. 准备工作 生成一个TypeScript的基础架子: // 创建目录 mkdir ts-event-bus && cd ts-event-bus // 初始化工程 yarn init -y // 安装typescript yar

  • TypeScript新语法之infer extends示例详解

    目录 正文 模式匹配 提取枚举的值的类型 总结 正文 我们知道,TypeScript 支持 infer 来提取类型的一部分,通过模式匹配的方式. 模式匹配 比如元组类型提取最后一个元素的类型: type Last<Arr extends unknown[]> = Arr extends [...infer rest,infer Ele] ? Ele : never; 比如函数提取返回值类型: type GetReturnType<Func extends Function> = F

  • TypeScript类型断言VS类型守卫示例详解

    目录 类型断言 类型守卫 使用 in 关键字 使用 instanceof 关键字 使用 typeof 关键字 自定义类型守卫 总结 类型断言 类型断言有两种写法,分别为value as Type和<Type>value,它让 TypeScript 编译器将 value 当作 Type 类型.类型断言是一个编译时特性,不进行类型转换,因此不会影响变量在运行时的数据类型.如果某变量是 any 类型,但现在你知道它确切的数据类型,使用类型断言能让 IDE 有代码提示的能力,也能让 TypeScrip

  • TypeScript类型级别和值级别示例详解

    目录 对值级别编程类型级别编程区分 类型级编程 挑战是如何工作的 挑战 对值级别编程类型级别编程区分 首先,让我们对值级别编程和类型级别编程进行重要区分. 值级别编程让我们编写将在生产中运行的代码即运行期,并为我们的用户提供有用的东西. 类型级别编程帮助我们确保代码在发布之前即编译期不包含错误,在运行期会被完全删除 JavaScript没有类型,所以所有JavaScript都是值级别的代码: // A simple Javascript function: function sum(a, b)

  • JS实现一个微信录音功能过程示例详解

    目录 功能原型图 拆解需求 评估时间 代码实现 功能原型图 其实就是微信发送语音的功能.没有转文字的功能. 拆解需求 根据原型图可以很容易的得出我们需要做的内容包括下面三个部分: 接入微信的语音SDK 调用微信SDK的API逻辑 界面和交互的实现 其中第一点和第二点属于业务逻辑部分,第三点属于交互逻辑部分.对于业务逻辑和交互逻辑的关系在我的另外一篇文章描述过,我在vue中是这样拆分组件的 从原型图可以分析出如下的流程图: 评估时间 第三事情是评估时间.在接到这个需求的时候,我们需要假设我们在此之

  • 使用typescript+webpack构建一个js库的示例详解

    目录 入口文件 tsconfig配置 webpack配置文件 webpack入口文件配置 webpack为typescript和less文件配置各自的loader webpack的output配置 运行webpack进行打包 测试验证 输出esm模块 已经输出了umd格式的js了, 为什么还要输出esm模块? ----TreeShaking 用tsc输出esm和类型声明文件 package.json中添加exports配置声明模块导出路径 完善package.json文件 用api-extrac

  • Go类型安全的HTTP请求示例详解

    目录 前言 Go 原生写法 httpc 实现 更多能力 前言 对 Gopher 来说,虽然我们基本都是在写代码让别人来请求,但是有时候,我们也需要去请求第三方提供的 RESTful 接口,这个时候,我们才能感受到前端同学拼接 HTTP 请求参数的痛苦. 比如,我们要发起类似这样一个请求,看起来很简单,实际写起来还是比较繁琐的. POST /articles/5/update?device=ios HTTP/1.1 Host: go-zero.dev Authorization: Bearer <

  • TypeScript 泛型推断实现示例详解

    目录 前言 基础类型准备 最终使用的方式 基于Interface的实现 (失败了) 所有内容都基于type 实现 完整Demo 结束语 前言 最近做东西都在用ts,有时候写比较复杂的功能,如果不熟悉,类型写起来还是挺麻烦的.有这样一个功能,在这里,我们就不以我们现有的业务来举例了,我们还是已Animal举例,来说明场景.通过一个工厂来创建不同的动物实例.在这里我们借助泛型来实现类型的约束和动态推到指定类型. 基础类型准备 用一个枚举来定义Animal的类型 enum EAnimalType {

  • TypeScript前端上传文件到MinIO示例详解

    目录 什么是MinIO? 本地Docker部署测试服务器 上传的API TypeScript实现 1. XMLHttpRequest 2. Fetch API 3. Axios 从后端获取临时上传链接 上传文件 踩过的坑 1. presignedPutObject方式上传提交的方法必须得是PUT 2. 直接发送File即可 3. 使用Axios上传的时候,需要自己把Content-Type填写成为file.type 示例代码 什么是MinIO? MinIO 是一款高性能.分布式的对象存储系统.

  • Typescript装饰器AOP示例详解

    目录 在Typescript中使用装饰器 配置 类装饰器 方法装饰器 AOP(面向切面编程) 在Typescript中使用装饰器 上文中讲了装饰模式,今天来来介绍一些Typescript里面的装饰器,以及如何用装饰器来实现之前提及装饰模式,装饰器只是实现装饰模式的一种方式,并非唯一 配置 在Typescript要使用装饰器需要在tsconfig打开装饰器的语法 "compilerOptions": { "experimentalDecorators": true }

随机推荐