当前位置: 代码网 > it编程>编程语言>Javascript > TypeScript实现类型安全的EventEmitter

TypeScript实现类型安全的EventEmitter

2024年05月15日 Javascript 我要评论
正文最近个人项目用 eventemitter 模块越来越多了,因为类型不够安全,写起来要很小心。所以打算改良一下,实现 typescript 类型安全的 eventemitter,解决事件名和函数类型

正文

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

nodejs 的 eventemitter 是一个发布订阅模块。

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

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

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

class eventemitter {
  constructor(options?: eventemitteroptions);
  // 类型过于宽泛
  on(eventname: string | symbol, listener: (...args: any[]) => void): this;
  emit(eventname: string | symbol, ...args: any[]): boolean;
  // ...其他
}

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

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

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

eventemitter 实现

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

class eventemitter {
  eventmap = {};

  // 添加对应事件的监听函数
  on(eventname, listener) {
    if (!this.eventmap[eventname]) {
      this.eventmap[eventname] = [];
    }
    this.eventmap[eventname].push(listener);
    return this;
  }

  // 触发事件
  emit(eventname, ...args) {
    const listeners = this.eventmap[eventname];
    if (!listeners || listeners.length === 0) return false;
    listeners.foreach((listener) => {
      listener(...args);
    });
    return true;
  }

  // 取消对应事件的监听
  off(eventname, listener) {
    const listeners = this.eventmap[eventname];
    if (listeners && listeners.length > 0) {
      const index = listeners.indexof(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
    return this;
  }
}

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

类型安全的 eventemitter

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

我们希望的效果是:

const ee = new eventemitter<{
  update(newval: string, prevval: string): void;
  destroy(): void;
}>();


const handler = (newval: string, prevval: string) => {
  console.log(newval, prevval)
}
ee.on("update", handler);
ee.emit('update', '前端西瓜哥上班前的精神状态', '前端西瓜哥上班后的精神状态')
ee.off("update", handler);

// 以下报错
// 'number' is not assignable to parameter of type 'string'
ee.emit('update', 1, 2)
// (val: number) => void' is not assignable to parameter of type '() => void
ee.on('destroy', (val: number) => {})

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

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

代码实现:

class eventemitter<t extends record<string | symbol, any>> {
  private eventmap: record<keyof t, array<(...args: any[]) => void>> =
    {} as any;

  // 添加对应事件的监听函数
  on<k extends keyof t>(eventname: k, listener: t[k]) {
    if (!this.eventmap[eventname]) {
      this.eventmap[eventname] = [];
    }
    this.eventmap[eventname].push(listener);
    return this;
  }

  // 触发事件
  emit<k extends keyof t>(eventname: k, ...args: parameters<t[k]>) {
    const listeners = this.eventmap[eventname];
    if (!listeners || listeners.length === 0) return false;
    listeners.foreach((listener) => {
      listener(...args);
    });
    return true;
  }

  // 取消对应事件的监听
  off<k extends keyof t>(eventname: k, listener: t[k]) {
    const listeners = this.eventmap[eventname];
    if (listeners && listeners.length > 0) {
      const index = listeners.indexof(listener);
      if (index > -1) {
        listeners.splice(index, 1);
      }
    }
    return this;
  }
}

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

简单讲解一下。

首先是开头的类型参数。

class eventemitter<t extends record<string | symbol, any>> {
  //
}

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

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

record<string | symbol, any>
// 等价于
{
  [key: string | symbol]: any
}

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

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

eventmap = {
  event1: [ handler1, handler2 ],
  event2: [ handler3, handler4 ]
}

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

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

private eventmap: record<keyof t, array<(...args: any[]) => void>> =
  {} as any;

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

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

on<k extends keyof t>(eventname: k, listener: t[k]): this

off 方法同理,不展开讲。

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

emit<k extends keyof t>(eventname: k, ...args: parameters<t[k]>): boolean

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

临时扩展自定义事件

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

interface events {
  update(newval: string, prevval: string): void;
  destroy(): void;
}
const ee = new eventemitter<events>();

// 用 & 扩展
const ee2 = ee as eventemitter<
  events & {
    customa(a: boolean): void;
  }
>;
// 不报错
ee2.emit('customa', true)

// 或者
(ee as eventemitter<
  events & {
    customa(a: boolean): void;
  }
>).emit('customa', true)

结尾

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

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

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

以上就是typescript实现类型安全的eventemitter的详细内容,更多关于ts eventemitter安全类型的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com