经验首页 前端设计 程序设计 Java相关 移动开发 数据库/运维 软件/图像 大数据/云计算 其他经验
当前位置:技术经验 » JS/JS库/框架 » TypeScript » 查看文章
TypeScript实现类型安全的EventEmitter
来源:jb51  时间:2023/3/8 10:59:02  对本文有异议

正文

最近个人项目用 EventEmitter 模块越来越多了,因为类型不够安全,写起来要很小心。所以打算改良一下,实现 TypeScript 类型安全的 EventEmitter,解决事件名和函数类型不能做检验的问题。

Nodejs 的 EventEmitter 是一个发布订阅模块。

利用该类,我们可以实现事件的监听,被监听对象会在合适的时机触发事件,调用监听对象提供的方法,是模块间解耦的常用实现。

配合越来越流行的 TypeScript,我们可以通过安装 @types/node,我们能够进一步获得类型能力,减少低级错误的出现。但 EventEmitter 的类型实现并不出色,称不上是类型安全。

通常来说,不同事件对应的响应函数类型是不同的,但 @types/nodeEventEmiiter 类型没有提供高级类型,而是给一个异常宽松的值

  1. class EventEmitter {
  2. constructor(options?: EventEmitterOptions);
  3. // 类型过于宽泛
  4. on(eventName: string | symbol, listener: (...args: any[]) => void): this;
  5. emit(eventName: string | symbol, ...args: any[]): boolean;
  6. // ...其他
  7. }

可以看到,on 方法传入的事件名类型是 string | symbol,listener 则是随意任何类型的一个函数即可。emit 传入的参数也是 any[]

因为过于宽松的类型,如果事件名拼错了,TypeScript 并不会报错,当一个 eventEmitter 的事件类型变得非常多,我们就和裸写 JavaScript 没什么区别了。

自己动手,丰衣足食,我们不妨 自己实现一个类型安全的 EventEmitter

EventEmitter 实现

因为我其实是在前端用的 EventEmitter,所以写了一个 EventEmitter 简易 JavaScript 实现。

  1. class EventEmitter {
  2. eventMap = {};
  3.  
  4. // 添加对应事件的监听函数
  5. on(eventName, listener) {
  6. if (!this.eventMap[eventName]) {
  7. this.eventMap[eventName] = [];
  8. }
  9. this.eventMap[eventName].push(listener);
  10. return this;
  11. }
  12.  
  13. // 触发事件
  14. emit(eventName, ...args) {
  15. const listeners = this.eventMap[eventName];
  16. if (!listeners || listeners.length === 0) return false;
  17. listeners.forEach((listener) => {
  18. listener(...args);
  19. });
  20. return true;
  21. }
  22.  
  23. // 取消对应事件的监听
  24. off(eventName, listener) {
  25. const listeners = this.eventMap[eventName];
  26. if (listeners && listeners.length > 0) {
  27. const index = listeners.indexOf(listener);
  28. if (index > -1) {
  29. listeners.splice(index, 1);
  30. }
  31. }
  32. return this;
  33. }
  34. }

如果你是 nodejs,继承 EventEmitter 然后改它的类型或许是更好的做法,或者可以 “基于组合而不是继承” 的方式实现一个。

类型安全的 EventEmitter

接着是将上面的代码改为 TypeScript。

我们希望的效果是:

  1. const ee = new EventEmitter<{
  2. update(newVal: string, prevVal: string): void;
  3. destroy(): void;
  4. }>();
  5.  
  6.  
  7. const handler = (newVal: string, prevVal: string) => {
  8. console.log(newVal, prevVal)
  9. }
  10. ee.on("update", handler);
  11. ee.emit('update', '前端西瓜哥上班前的精神状态', '前端西瓜哥上班后的精神状态')
  12. ee.off("update", handler);
  13.  
  14. // 以下报错
  15. // 'number' is not assignable to parameter of type 'string'
  16. ee.emit('update', 1, 2)
  17. // (val: number) => void' is not assignable to parameter of type '() => void
  18. ee.on('destroy', (val: number) => {})

EventEmitter 支持接受一个对象结构的 interface 作为类型参数,指定不同的 key 对应的函数类型。

然后我们再调用 on、emit、off 时,如果事件名、函数参数不匹配,编译就不能通过

代码实现:

  1. class EventEmitter<T extends Record<string | symbol, any>> {
  2. private eventMap: Record<keyof T, Array<(...args: any[]) => void>> =
  3. {} as any;
  4.  
  5. // 添加对应事件的监听函数
  6. on<K extends keyof T>(eventName: K, listener: T[K]) {
  7. if (!this.eventMap[eventName]) {
  8. this.eventMap[eventName] = [];
  9. }
  10. this.eventMap[eventName].push(listener);
  11. return this;
  12. }
  13.  
  14. // 触发事件
  15. emit<K extends keyof T>(eventName: K, ...args: Parameters<T[K]>) {
  16. const listeners = this.eventMap[eventName];
  17. if (!listeners || listeners.length === 0) return false;
  18. listeners.forEach((listener) => {
  19. listener(...args);
  20. });
  21. return true;
  22. }
  23.  
  24. // 取消对应事件的监听
  25. off<K extends keyof T>(eventName: K, listener: T[K]) {
  26. const listeners = this.eventMap[eventName];
  27. if (listeners && listeners.length > 0) {
  28. const index = listeners.indexOf(listener);
  29. if (index > -1) {
  30. listeners.splice(index, 1);
  31. }
  32. }
  33. return this;
  34. }
  35. }

读者朋友可自行拷贝上面两段代码到 TypeScript Playground 测试一下。

简单讲解一下。

首先是开头的类型参数。

  1. class EventEmitter<T extends Record<string | symbol, any>> {
  2. //
  3. }

这里的 extends 作用是限定类型范围,防止提供一个不符合规则的类型参数。

Record 是 TypeScript 自带的高级类型,根据传入的 key 和 value 创建一个对象结构(后面说到的 T 就是它)。

  1. Record<string | symbol, any>
  2. // 等价于
  3. {
  4. [key: string | symbol]: any
  5. }

value 本来的类型应该是 (...args: any[]) => void,好限制为函数。但在不是非字面量类型直传的情况下无法通过类型检测,只好改成 any 了。(坑爹的 Index signature for type 'string' is missing 报错)

然后是 eventMap,它的实际内容是这样的:

  1. eventMap = {
  2. event1: [ handler1, handler2 ],
  3. event2: [ handler3, handler4 ]
  4. }

所以 key 需要为传入对象类型参数的 key。

函数则不用指定特定类型,因为它是私有的,无法被类外部访问,没有做过多的类型推断,就宽松一些,设置为任何函数类型。

  1. private eventMap: Record<keyof T, Array<(...args: any[]) => void>> =
  2. {} as any;

这里我用了对象字面量,读者朋友也可以考虑用 Map 数据结构。

然后是 on 方法,首先 eventName 必须为 T 的 key 的其中之一,因为要推断 K 这么个内部类型变量,所以我们要在 on 后面加上 <K extends keyof T>,listener 就是对应的 T[K]

  1. on<K extends keyof T>(eventName: K, listener: T[K]): this

off 方法同理,不展开讲。

然后是 emit,第一个 eventName 用 keyof T 没问题,后面需要取出 handler 的参数,作为剩余参数。

  1. emit<K extends keyof T>(eventName: K, ...args: Parameters<T[K]>): boolean

这里用了 TS 自带的 Parameters 高级类型,作用是取出函数的参数返回一个数组类型。

临时扩展自定义事件

如果要给一个已经固定了类型的实例,临时加一个事件,可以用 & 交叉类型扩展一下。

  1. interface Events {
  2. update(newVal: string, prevVal: string): void;
  3. destroy(): void;
  4. }
  5. const ee = new EventEmitter<Events>();
  6.  
  7. // 用 & 扩展
  8. const ee2 = ee as EventEmitter<
  9. Events & {
  10. customA(a: boolean): void;
  11. }
  12. >;
  13. // 不报错
  14. ee2.emit('customA', true)
  15.  
  16. // 或者
  17. (ee as EventEmitter<
  18. Events & {
  19. customA(a: boolean): void;
  20. }
  21. >).emit('customA', true)

结尾

一番改造,我们充分利用 TypeScript 的强大类型体操能力,构建了一个类型安全的 EventEmitter。写错事件名,函数类型没对上什么的,根本不在怕的。

这次的类型体操还算是比较简单的。如果再复杂一点,可读性就很差了。

TypeScript 的类型编程的语法真的很不美观,可读性差。如果你不是库作者,个人不建议过度使用类型体操,它像正则一样,很强大,但也很复杂。

以上就是TypeScript实现类型安全的EventEmitter的详细内容,更多关于TS EventEmitter安全类型的资料请关注w3xue其它相关文章!

 友情链接:直通硅谷  点职佳  北美留学生论坛

本站QQ群:前端 618073944 | Java 606181507 | Python 626812652 | C/C++ 612253063 | 微信 634508462 | 苹果 692586424 | C#/.net 182808419 | PHP 305140648 | 运维 608723728

W3xue 的所有内容仅供测试,对任何法律问题及风险不承担任何责任。通过使用本站内容随之而来的风险与本站无关。
关于我们  |  意见建议  |  捐助我们  |  报错有奖  |  广告合作、友情链接(目前9元/月)请联系QQ:27243702 沸活量
皖ICP备17017327号-2 皖公网安备34020702000426号