使用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內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- TypeScript實現類型安全的EventEmitter
- TypeScript基本類型之typeof和keyof詳解
- 淺談typescript中keyof與typeof操作符用法
- Node中的Events模塊介紹及應用
- Vue3.x使用mitt.js進行組件通信