如何使用TypeScript实现一个类型安全的EventBus
这篇文章主要介绍“如何使用TypeScript实现一个类型安全的EventBus”,在日常操作中,相信很多人在如何使用TypeScript实现一个类型安全的EventBus问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答”如何使用TypeScript实现一个类型安全的EventBus”的疑惑有所帮助!接下来,请跟着小编一起来学习吧!
准备工作
生成一个TypeScript
的基础架子:
// 创建目录mkdir ts-event-bus && cd ts-event-bus// 初始化工程yarn init -y// 安装typescriptyarn 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>> { 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);});// 输出 hellobus.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 => hellobus.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 => hellobus.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 => 123bus.off();bus.emit("foo", "hello");bus.emit("bar", 123);// 什么都没输出
从测试结果来看,我们的off
方法功能也没问题,这样就完成了我们的EventBus
。
此外,我们还可以给我们的方法加上注释,这样在我们鼠标移到api上方和我们输入参数的时候,编辑器就会有提示。
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; off<EventName extends keyof Events>(name: EventName): void; 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(); 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); } 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; off<EventName extends keyof Events>(name: EventName): void; 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 => hellobus.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 => hellobus.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 => 123bus.off();bus.emit("foo", "hello");bus.emit("bar", 123);// 什么都没输出
到此,关于“如何使用TypeScript实现一个类型安全的EventBus”的学习就结束了,希望能够解决大家的疑惑。理论与实践的搭配能更好的帮助大家学习,快去试试吧!若想继续学习更多相关知识,请继续关注编程网网站,小编会继续努力为大家带来更多实用的文章!
免责声明:
① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。
② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341