使用TypeScript實(shí)現(xiàn)一個(gè)類(lèi)型安全的EventBus示例詳解
前言
隨著vue3的發(fā)布,TypeScript在國(guó)內(nèi)越來(lái)越流行,學(xué)習(xí)TypeScript也隨即變成了大勢(shì)所趨。本文就通過(guò)實(shí)現(xiàn)一個(gè)類(lèi)型安全的EventBus來(lái)練習(xí)TypeScript,希望對(duì)小伙伴們有所幫助。
準(zhǔn)備工作
生成一個(gè)TypeScript的基礎(chǔ)架子:
// 創(chuàng)建目錄 mkdir ts-event-bus && cd ts-event-bus // 初始化工程 yarn init -y // 安裝typescript yarn add typescript -D // 生成typescript配置文件 npx tsc --init
這樣一來(lái)我們就搭建好了一個(gè)TypeScript的基礎(chǔ)架子,為了方便我們后續(xù)的測(cè)試,我們需要下載ts-node,它可以讓我們?cè)诓痪幾gTypeScript代碼的情況下運(yùn)行TypeScript。
yarn add ts-node -D
目標(biāo)
- 基礎(chǔ)功能完備,包括注冊(cè),發(fā)布,取消訂閱三個(gè)核心功能。
- 類(lèi)型安全,能約束我們輸入的參數(shù),并且有代碼提示。
思路
每一個(gè)Event都可以注冊(cè)多個(gè)處理函數(shù),我們用一個(gè)Set來(lái)保存這些處理函數(shù),再用一個(gè)Map來(lái)保存Event到對(duì)應(yīng)Set的映射,如圖所示:

具體實(shí)現(xiàn)
// 定義泛型函數(shù)類(lèi)型
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);
}
}這里我們分成邏輯和類(lèi)型兩方面來(lái)講
邏輯方面,我們初始化了一個(gè)空的Map,然后當(dāng)調(diào)用on 用來(lái)注冊(cè)事件的時(shí)候,先去根據(jù)EventName來(lái)找有沒(méi)有對(duì)應(yīng)的Set,沒(méi)有就創(chuàng)建一個(gè),并且把事件添加到Set中,這一部分的代碼相當(dāng)簡(jiǎn)單,實(shí)現(xiàn)起來(lái)也沒(méi)什么難度。
類(lèi)型方面,我們將EventBus 定義為一個(gè)泛型類(lèi),并約束泛型為 Events extends Record<string, any>,這樣就約束了傳入的泛型參數(shù)必須是一個(gè)對(duì)象類(lèi)型,例如:
type Events = {
foo : number;
bar : string;
}我們可以通過(guò)這個(gè)類(lèi)型來(lái)獲取key對(duì)應(yīng)value的類(lèi)型
// number; type ValueTypeOfFoo = Events['foo']
進(jìn)而可以獲取foo事件對(duì)應(yīng)的handler函數(shù)的類(lèi)型,即:
// (val:number) => void; type HandlerOfFoo = Handler<Events['foo']>
我們又將on方法設(shè)置為泛型函數(shù),同時(shí)約束EventName extends keyof Events,這樣一來(lái)Events[EventName] 就是對(duì)應(yīng)值的類(lèi)型,Handler<Events[EventName]>就是處理函數(shù)的類(lèi)型。通過(guò)這樣的方式我們實(shí)現(xiàn)了一個(gè)類(lèi)型安全的on方法。
接著我們編寫(xiě)一段代碼測(cè)試一下
可以看到,我們?cè)趘scode中編寫(xiě)代碼的時(shí)候,編輯器能給我們代碼提示了。

我們鍵入handler函數(shù),編輯器也會(huì)提醒我們val是一個(gè)string類(lèi)型。

當(dāng)我們傳的參數(shù)不合法的時(shí)候,TypeScript也會(huì)給我們警告


接下來(lái)我們依葫蘆畫(huà)瓢實(shí)現(xiàn)emit函數(shù)。
class EventBus<Events extends Record<string, any>> {
... others code
/** 觸發(fā)事件 */
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對(duì)應(yīng)的Set,如果有就取出并依次執(zhí)行。這里的邏輯也相當(dāng)簡(jiǎn)單,我們編寫(xiě)代碼測(cè)試一下
const bus = new EventBus<{
foo: string;
bar: number;
}>();
bus.on("foo", (val) => {
console.log(val);
});
// 輸出 hello
bus.emit("foo", "hello");我們?cè)诮K端運(yùn)行npx ts-node ./index.ts,輸出hello,說(shuō)明我們的程序已經(jīng)生效。
接下來(lái)我們實(shí)現(xiàn)取消訂閱的功能。
{
...
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);
}
}取消訂閱我們這樣設(shè)計(jì),它傳入0至2個(gè)參數(shù),什么都不傳代表清除所有事件,只傳一個(gè)參數(shù)代表清除同名事件,傳兩個(gè)參數(shù)代表只清除該事件指定的處理函數(shù),所以它的兩個(gè)參數(shù)都是可選的,實(shí)現(xiàn)的邏輯也非常簡(jiǎn)單,我們這里不多贅述。
我們編寫(xiě)一段測(cè)試代碼看下效果
const bus = new EventBus<{
foo: string;
bar: number;
}>();
// 測(cè)試傳2個(gè)參數(shù)的情況
const handlerFoo1 = (val: string) => {
console.log("2個(gè)參數(shù) handlerFoo1 => ", val);
};
bus.on("foo", handlerFoo1);
bus.emit("foo", "hello");
// 打印 2個(gè)參數(shù) handlerFoo1 => hello
bus.off("foo", handlerFoo1);
bus.emit("foo", "hello");
// 什么都沒(méi)打印
// 測(cè)試傳1個(gè)參數(shù)的情況
const handlerFoo2 = (val: string) => {
console.log("1個(gè)參數(shù) handlerFoo2 => ", val);
};
const handlerFoo3 = (val: string) => {
console.log("1個(gè)參數(shù) handlerFoo3 => ", val);
};
bus.on("foo", handlerFoo2);
bus.on("foo", handlerFoo3);
bus.emit("foo", "hello");
// 打印 1個(gè)參數(shù) handlerFoo2 => hello
// 打印 1個(gè)參數(shù) handlerFoo3 => hello
bus.off("foo");
bus.emit("foo", "hello");
// 什么都沒(méi)輸出
// 測(cè)試傳0個(gè)參數(shù)的情況
const handlerFoo4 = (val: string) => {
console.log("0個(gè)參數(shù) handlerFoo4 => ", val);
};
const handlerBar1 = (val: number) => {
console.log("0個(gè)參數(shù) handlerBar1 => ", val);
};
bus.on("foo", handlerFoo4);
bus.on("bar", handlerBar1);
bus.emit("foo", "hello");
bus.emit("bar", 123);
// 打印 1個(gè)參數(shù) handlerFoo4 => hello
// 打印 1個(gè)參數(shù) handlerBar1 => 123
bus.off();
bus.emit("foo", "hello");
bus.emit("bar", 123);
// 什么都沒(méi)輸出
從測(cè)試結(jié)果來(lái)看,我們的off方法功能也沒(méi)問(wèn)題,這樣就完成了我們的EventBus。
此外,我們還可以給我們的方法加上注釋?zhuān)@樣在我們鼠標(biāo)移到api上方和我們輸入?yún)?shù)的時(shí)候,編輯器就會(huì)有提示。
/**
* 訂閱事件
* @param name 事件名
* @param handler 事件處理函數(shù)
*/
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);
}

可以看到,編輯器給我們提供了很好的提示,極大方便了我們的編碼。
我們還可以用函數(shù)重載來(lái)改進(jìn)我們的off方法,以獲得更友好的提示
{
/**
* 清除所有事件
*/
off(): void;
/**
* 清除同名事件
* @param name 事件名
*/
off<EventName extends keyof Events>(name: EventName): void;
/**
* 清除指定事件
* @param name 事件名
* @param handler 事件處理函數(shù)
*/
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);
}
}
改造前的提示:

改造后的提示:

至此,我們就完成了一個(gè)功能完備,類(lèi)型安全的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 事件處理函數(shù)
*/
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);
}
/**
* 觸發(fā)事件
* @param name 事件名
* @param handler 事件處理函數(shù)
*/
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 處理函數(shù)
*/
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;
}>();
// 測(cè)試傳2個(gè)參數(shù)的情況
const handlerFoo1 = (val: string) => {
console.log("2個(gè)參數(shù) handlerFoo1 => ", val);
};
bus.on("foo", handlerFoo1);
bus.emit("foo", "hello");
// 打印 2個(gè)參數(shù) handlerFoo1 => hello
bus.off("foo", handlerFoo1);
bus.emit("foo", "hello");
// 什么都沒(méi)打印
// 測(cè)試傳1個(gè)參數(shù)的情況
const handlerFoo2 = (val: string) => {
console.log("1個(gè)參數(shù) handlerFoo2 => ", val);
};
const handlerFoo3 = (val: string) => {
console.log("1個(gè)參數(shù) handlerFoo3 => ", val);
};
bus.on("foo", handlerFoo2);
bus.on("foo", handlerFoo3);
bus.emit("foo", "hello");
// 打印 1個(gè)參數(shù) handlerFoo2 => hello
// 打印 1個(gè)參數(shù) handlerFoo3 => hello
bus.off("foo");
bus.emit("foo", "hello");
// 什么都沒(méi)輸出
// 測(cè)試傳0個(gè)參數(shù)的情況
const handlerFoo4 = (val: string) => {
console.log("0個(gè)參數(shù) handlerFoo4 => ", val);
};
const handlerBar1 = (val: number) => {
console.log("0個(gè)參數(shù) handlerBar1 => ", val);
};
bus.on("foo", handlerFoo4);
bus.on("bar", handlerBar1);
bus.emit("foo", "hello");
bus.emit("bar", 123);
// 打印 1個(gè)參數(shù) handlerFoo4 => hello
// 打印 1個(gè)參數(shù) handlerBar1 => 123
bus.off();
bus.emit("foo", "hello");
bus.emit("bar", 123);
// 什么都沒(méi)輸出
后記
EventBus是工作中常用的工具,本文用Typescript實(shí)現(xiàn)一個(gè)具備基礎(chǔ)功能且類(lèi)型安全的EventBus,是我近期學(xué)習(xí)Typescript的知識(shí)總結(jié),希望小伙伴們有所幫助。
本文的代碼已同步到GitHub上,喜歡的同學(xué)可以 clone 下來(lái)學(xué)習(xí),如果喜歡那就點(diǎn)個(gè)??吧!
到此這篇關(guān)于用TypeScript實(shí)現(xiàn)一個(gè)類(lèi)型安全的EventBus的文章就介紹到這了,更多相關(guān)TypeScript實(shí)現(xiàn)類(lèi)型安全的EventBus內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
js點(diǎn)擊時(shí)關(guān)閉該范圍下拉菜單之外的菜單方法
下面小編就為大家分享一篇js點(diǎn)擊時(shí)關(guān)閉該范圍下拉菜單之外的菜單方法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2018-01-01
JS實(shí)現(xiàn)的簡(jiǎn)單拖拽功能示例
這篇文章主要介紹了JS實(shí)現(xiàn)的簡(jiǎn)單拖拽功能,涉及javascript鼠標(biāo)事件響應(yīng)及頁(yè)面元素屬性動(dòng)態(tài)操作相關(guān)技巧,需要的朋友可以參考下2017-03-03
JavaScript實(shí)現(xiàn)的多種鼠標(biāo)拖放效果
這篇文章主要介紹了JavaScript實(shí)現(xiàn)的多種鼠標(biāo)拖放效果,涉及JavaScript響應(yīng)鼠標(biāo)事件動(dòng)態(tài)變換頁(yè)面元素屬性的相關(guān)技巧,具有一定參考借鑒價(jià)值,需要的朋友可以參考下2015-11-11
js定時(shí)器出現(xiàn)第一次延遲的原因及解決方法
在本篇文章里小編給大家整理的是一篇關(guān)于js定時(shí)器出現(xiàn)第一次延遲的原因及解決方法,對(duì)此有需要的朋友們可以學(xué)習(xí)下。2021-01-01
關(guān)于JavaScript奇怪又實(shí)用的六個(gè)姿勢(shì)
這篇文章主要給大家介紹了關(guān)于JavaScript奇怪又實(shí)用的六個(gè)姿勢(shì),這些技巧和建議是我平常在開(kāi)發(fā)項(xiàng)目上會(huì)用到的,希望能讓大家學(xué)到知識(shí),需要的朋友可以參考下2021-10-10
JavaScript中的console.profile()函數(shù)詳細(xì)介紹
這篇文章主要介紹了JavaScript中的console.profile()函數(shù)詳細(xì)介紹,本文講解了console.profile()函數(shù)的瀏覽器支持情況、console.profile()的使用、Firebug中Profile按鈕的使用等內(nèi)容,需要的朋友可以參考下2014-12-12
JavaScript中setInterval的用法總結(jié)
這篇文章主要是對(duì)JavaScript中setInterval的用法進(jìn)行了詳細(xì)的總結(jié)介紹,需要的朋友可以過(guò)來(lái)參考下,希望對(duì)大家有所幫助2013-11-11

