前言

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

使用TypeScript实现一个类型安全的EventBus示例详解的更多相关文章

  1. 基于 HTML5 WebGL 实现的医疗物流系统

    物联网( IoT ),简单的理解就是物体之间通过互联网进行链接。这篇文章给大家介绍基于 HTML5 WebGL 实现的医疗物流系统,感兴趣的朋友跟随小编一起看看吧

  2. 浅析Otto框架,并与EventBus对比

    前两天在公众号里发了一篇有关EventBus的文章《玩转EventBus,详解其使用》,有读者和开发者反馈说没有ottO好用。与EventBus的对比从事件订阅的处理差别来看:1、eventbus是采用反射的方式对整个注册的类的所有方法进行扫描来完成注册;2、otto采用了注解的方式完成注册;3、共同的地方缓存所有注册并有可用性的检测。

  3. android – 如何使用EventBus调用Call Type

    当我从服务器得到响应时,我正在使用EventBus通知Activity/Fragment.到目前为止,一切都运行良好,但是当我在相同的Fragment或Activity中使用两个网络调用时会出现问题.问题是onEvent(Stringresponse)获取来自服务器的两个响应的调用方法.呼叫1的响应与呼叫2不同.我提出了一个解决方案–我在NetworkReqest中添加了CallType但是我无法

  4. 使用typescript类型实现ThreeSum

    这篇文章主要介绍了使用typescript类型实现ThreeSum,文章围绕主题展开详细的内容介绍,具有一定的参考价值,需要的小伙伴可以一下,希望对你学习又是帮助

  5.  typeScript入门基础介绍

    这篇文章主要介绍了 typeScript入门基础,TypeScript 是由微软开发的开源、跨平台的编程语言,是 javaScript 的超集,最终被编译为 javaScript代码。常常被简称为TS支持JS、ES语法,下文将继续其他基础介绍,需要的朋友可以参考一下

  6. typescript返回值类型和参数类型的具体使用

    本文主要介绍了typescript返回值类型和参数类型的具体使用文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

  7. Vue3 携手 TypeScript 搭建完整项目结构

    TypeScript 是JS的一个超级,主要提供了类型系统和对ES6的支持,使用 TypeScript 可以增加代码的可读性和可维护性,在 react 和 vue 社区中也越来越多人开始使用TypeScript,这篇文章主要介绍了Vue3 携手 TypeScript 搭建完整项目结构,需要的朋友可以参考下

  8. 关于TypeScript开发的6六个实用小技巧分享

    TypeScript是Javascrip t超集,支持静态类型检测,可以在编译期提前暴露问题,节省debug时间,下面这篇文章主要给大家介绍了关于TypeScript开发的6六个实用小技巧,需要的朋友可以参考下

  9. typescript+react实现移动端和PC端简单拖拽效果

    这篇文章主要为大家详细介绍了typescript+react实现移动端和PC端简单拖拽效果,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. Angular5集成eventbus的示例代码

    这篇文章主要介绍了Angular5集成eventbus的示例代码,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧

随机推荐

  1. js中‘!.’是什么意思

  2. Vue如何指定不编译的文件夹和favicon.ico

    这篇文章主要介绍了Vue如何指定不编译的文件夹和favicon.ico,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教

  3. 基于JavaScript编写一个图片转PDF转换器

    本文为大家介绍了一个简单的 JavaScript 项目,可以将图片转换为 PDF 文件。你可以从本地选择任何一张图片,只需点击一下即可将其转换为 PDF 文件,感兴趣的可以动手尝试一下

  4. jquery点赞功能实现代码 点个赞吧!

    点赞功能很多地方都会出现,如何实现爱心点赞功能,这篇文章主要为大家详细介绍了jquery点赞功能实现代码,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  5. AngularJs上传前预览图片的实例代码

    使用AngularJs进行开发,在项目中,经常会遇到上传图片后,需在一旁预览图片内容,怎么实现这样的功能呢?今天小编给大家分享AugularJs上传前预览图片的实现代码,需要的朋友参考下吧

  6. JavaScript面向对象编程入门教程

    这篇文章主要介绍了JavaScript面向对象编程的相关概念,例如类、对象、属性、方法等面向对象的术语,并以实例讲解各种术语的使用,非常好的一篇面向对象入门教程,其它语言也可以参考哦

  7. jQuery中的通配符选择器使用总结

    通配符在控制input标签时相当好用,这里简单进行了jQuery中的通配符选择器使用总结,需要的朋友可以参考下

  8. javascript 动态调整图片尺寸实现代码

    在自己的网站上更新文章时一个比较常见的问题是:文章插图太宽,使整个网页都变形了。如果对每个插图都先进行缩放再插入的话,太麻烦了。

  9. jquery ajaxfileupload异步上传插件

    这篇文章主要为大家详细介绍了jquery ajaxfileupload异步上传插件,具有一定的参考价值,感兴趣的小伙伴们可以参考一下

  10. React学习之受控组件与数据共享实例分析

    这篇文章主要介绍了React学习之受控组件与数据共享,结合实例形式分析了React受控组件与组件间数据共享相关原理与使用技巧,需要的朋友可以参考下

返回
顶部