<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
最近個人專案用 EventEmitter 模組越來越多了,因為型別不夠安全,寫起來要很小心。所以打算改良一下,實現 TypeScript 型別安全的 EventEmitter,解決事件名和函數型別不能做檢驗的問題。
Nodejs 的 EventEmitter 是一個釋出訂閱模組。
利用該類,我們可以實現事件的監聽,被監聽物件會在合適的時機觸發事件,呼叫監聽物件提供的方法,是模組間解耦的常用實現。
配合越來越流行的 TypeScript,我們可以通過安裝 @types/node
,我們能夠進一步獲得型別能力,減少低階錯誤的出現。但 EventEmitter 的型別實現並不出色,稱不上是型別安全。
通常來說,不同事件對應的響應函數型別是不同的,但 @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 則是隨意任何型別的一個函數即可。emit 傳入的引數也是 any[]
。
因為過於寬鬆的型別,如果事件名拼錯了,TypeScript 並不會報錯,當一個 eventEmitter 的事件型別變得非常多,我們就和裸寫 JavaScript 沒什麼區別了。
自己動手,豐衣足食,我們不妨 自己實現一個型別安全的 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 然後改它的型別或許是更好的做法,或者可以 “基於組合而不是繼承” 的方式實現一個。
接著是將上面的程式碼改為 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安全型別的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45