TypeScript實現(xiàn)類型安全的EventEmitter
正文
最近個人項目用 EventEmitter 模塊越來越多了,因為類型不夠安全,寫起來要很小心。所以打算改良一下,實現(xiàn) TypeScript 類型安全的 EventEmitter,解決事件名和函數(shù)類型不能做檢驗的問題。
Nodejs 的 EventEmitter 是一個發(fā)布訂閱模塊。
利用該類,我們可以實現(xiàn)事件的監(jiān)聽,被監(jiān)聽對象會在合適的時機觸發(fā)事件,調(diào)用監(jiān)聽對象提供的方法,是模塊間解耦的常用實現(xiàn)。
配合越來越流行的 TypeScript,我們可以通過安裝 @types/node
,我們能夠進一步獲得類型能力,減少低級錯誤的出現(xiàn)。但 EventEmitter 的類型實現(xiàn)并不出色,稱不上是類型安全。
通常來說,不同事件對應(yīng)的響應(yīng)函數(shù)類型是不同的,但 @types/node
的 EventEmiiter 類型沒有提供高級類型,而是給一個異常寬松的值。
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 則是隨意任何類型的一個函數(shù)即可。emit 傳入的參數(shù)也是 any[]
。
因為過于寬松的類型,如果事件名拼錯了,TypeScript 并不會報錯,當(dāng)一個 eventEmitter 的事件類型變得非常多,我們就和裸寫 JavaScript 沒什么區(qū)別了。
自己動手,豐衣足食,我們不妨 自己實現(xiàn)一個類型安全的 EventEmitter。
EventEmitter 實現(xiàn)
因為我其實是在前端用的 EventEmitter,所以寫了一個 EventEmitter 簡易 JavaScript 實現(xiàn)。
class EventEmitter { eventMap = {}; // 添加對應(yīng)事件的監(jiān)聽函數(shù) on(eventName, listener) { if (!this.eventMap[eventName]) { this.eventMap[eventName] = []; } this.eventMap[eventName].push(listener); return this; } // 觸發(fā)事件 emit(eventName, ...args) { const listeners = this.eventMap[eventName]; if (!listeners || listeners.length === 0) return false; listeners.forEach((listener) => { listener(...args); }); return true; } // 取消對應(yīng)事件的監(jiān)聽 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 然后改它的類型或許是更好的做法,或者可以 “基于組合而不是繼承” 的方式實現(xiàn)一個。
類型安全的 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', '前端西瓜哥上班前的精神狀態(tài)', '前端西瓜哥上班后的精神狀態(tài)') 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 支持接受一個對象結(jié)構(gòu)的 interface 作為類型參數(shù),指定不同的 key 對應(yīng)的函數(shù)類型。
然后我們再調(diào)用 on、emit、off 時,如果事件名、函數(shù)參數(shù)不匹配,編譯就不能通過。
代碼實現(xiàn):
class EventEmitter<T extends Record<string | symbol, any>> { private eventMap: Record<keyof T, Array<(...args: any[]) => void>> = {} as any; // 添加對應(yīng)事件的監(jiān)聽函數(shù) on<K extends keyof T>(eventName: K, listener: T[K]) { if (!this.eventMap[eventName]) { this.eventMap[eventName] = []; } this.eventMap[eventName].push(listener); return this; } // 觸發(fā)事件 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; } // 取消對應(yīng)事件的監(jiān)聽 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 測試一下。
簡單講解一下。
首先是開頭的類型參數(shù)。
class EventEmitter<T extends Record<string | symbol, any>> { // }
這里的 extends 作用是限定類型范圍,防止提供一個不符合規(guī)則的類型參數(shù)。
Record 是 TypeScript 自帶的高級類型,根據(jù)傳入的 key 和 value 創(chuàng)建一個對象結(jié)構(gòu)(后面說到的 T 就是它)。
Record<string | symbol, any> // 等價于 { [key: string | symbol]: any }
value 本來的類型應(yīng)該是 (...args: any[]) => void
,好限制為函數(shù)。但在不是非字面量類型直傳的情況下無法通過類型檢測,只好改成 any 了。(坑爹的 Index signature for type 'string' is missing
報錯)
然后是 eventMap,它的實際內(nèi)容是這樣的:
eventMap = { event1: [ handler1, handler2 ], event2: [ handler3, handler4 ] }
所以 key 需要為傳入對象類型參數(shù)的 key。
函數(shù)則不用指定特定類型,因為它是私有的,無法被類外部訪問,沒有做過多的類型推斷,就寬松一些,設(shè)置為任何函數(shù)類型。
private eventMap: Record<keyof T, Array<(...args: any[]) => void>> = {} as any;
這里我用了對象字面量,讀者朋友也可以考慮用 Map 數(shù)據(jù)結(jié)構(gòu)。
然后是 on 方法,首先 eventName 必須為 T 的 key 的其中之一,因為要推斷 K 這么個內(nèi)部類型變量,所以我們要在 on 后面加上 <K extends keyof T>
,listener 就是對應(yīng)的 T[K]
。
on<K extends keyof T>(eventName: K, listener: T[K]): this
off 方法同理,不展開講。
然后是 emit,第一個 eventName 用 keyof T
沒問題,后面需要取出 handler 的參數(shù),作為剩余參數(shù)。
emit<K extends keyof T>(eventName: K, ...args: Parameters<T[K]>): boolean
這里用了 TS 自帶的 Parameters 高級類型,作用是取出函數(shù)的參數(shù)返回一個數(shù)組類型。
臨時擴展自定義事件
如果要給一個已經(jīng)固定了類型的實例,臨時加一個事件,可以用 &
交叉類型擴展一下。
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)
結(jié)尾
一番改造,我們充分利用 TypeScript 的強大類型體操能力,構(gòu)建了一個類型安全的 EventEmitter。寫錯事件名,函數(shù)類型沒對上什么的,根本不在怕的。
這次的類型體操還算是比較簡單的。如果再復(fù)雜一點,可讀性就很差了。
TypeScript 的類型編程的語法真的很不美觀,可讀性差。如果你不是庫作者,個人不建議過度使用類型體操,它像正則一樣,很強大,但也很復(fù)雜。
以上就是TypeScript實現(xiàn)類型安全的EventEmitter的詳細內(nèi)容,更多關(guān)于TS EventEmitter安全類型的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
ts?類型體操?Chainable?Options?可鏈?zhǔn)竭x項示例詳解
這篇文章主要為大家介紹了ts?類型體操?Chainable?Options?可鏈?zhǔn)竭x項示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-09-09TypeScript數(shù)據(jù)結(jié)構(gòu)鏈表結(jié)構(gòu)?LinkedList教程及面試
這篇文章主要為大家介紹了TypeScript數(shù)據(jù)結(jié)構(gòu)鏈表結(jié)構(gòu)?LinkedList教程及面試,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-02-02Manipulation-TypeScript?DOM操作示例解析
這篇文章主要為大家介紹了DOM?Manipulation-TypeScript?DOM操作示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-03-03TypeScript中的數(shù)據(jù)類型enum?type?interface基礎(chǔ)用法示例
這篇文章主要為大家介紹了TypeScript中的數(shù)據(jù)類型enum?type?interface基礎(chǔ)用法示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08Typescript?extends?關(guān)鍵字繼承類型約束及條件類型判斷實現(xiàn)示例解析
這篇文章主要介紹了Typescript?extends?關(guān)鍵字繼承類型約束及條件類型判斷實現(xiàn)示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08TypeScript類型編程中的extends和infer示例解析
這篇文章主要為大家介紹了TypeScript類型編程中的extends和infer示例解析,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-08-08typescript難學(xué)嗎?前端有必要學(xué)?該怎么學(xué)typescript
TypeScript代碼與?JavaScript?代碼有非常高的兼容性,無門檻,你把?JS?代碼改為?TS?就可以運行。TypeScript?應(yīng)該不會脫離?JavaScript?成為獨立的語言。學(xué)習(xí)?TypeScript?應(yīng)該主要指的是學(xué)習(xí)它的類型系統(tǒng)。2022-12-12