原文:https://dushusir.com/js-event...
介紹
Event Bus
事件匯流排,通常作為多個模組間的通訊機制,相當於一個事件管理中心,一個模組傳送訊息,其它模組接受訊息,就達到了通訊的作用。
比如,Vue 元件間的資料傳遞可以使用一個 Event Bus
來通訊,也可以用作微核心外掛系統中的外掛和核心通訊。
原理
Event Bus
本質上是採用了釋出-訂閱的設計模式,比如多個模組 A
、B
、C
訂閱了一個事件 EventX
,然後某一個模組 X
在事件匯流排釋出了這個事件,那麼事件匯流排會負責通知所有訂閱者 A
、B
、C
,它們都能收到這個通知訊息,同時還可以傳遞引數。
// 關係圖
模組X
⬇釋出EventX
╔════════════════════════════════════════════════════════════════════╗
║ Event Bus ║
║ ║
║ 【EventX】 【EventY】 【EventZ】 ... ║
╚════════════════════════════════════════════════════════════════════╝
⬆訂閱EventX ⬆訂閱EventX ⬆訂閱EventX
模組A 模組B 模組C
分析
如何使用 JavaScript 來實現一個簡單版本的 Event Bus
- 首先構造一個
EventBus
類,初始化一個空物件用於存放所有的事件 - 在接受訂閱時,將事件名稱作為 key 值,將需要在接受釋出訊息後執行的回撥函式作為 value 值,由於一個事件可能有多個訂閱者,所以這裡的回撥函式要儲存成列表
- 在釋出事件訊息時,從事件列表裡取得指定的事件名稱對應的所有回撥函式,依次觸發執行即可
以下是程式碼詳細實現,可以複製到谷歌瀏覽器控制檯直接執行檢測效果。
程式碼
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
}
// 釋出事件
publish(eventName) {
// 取出當前事件所有的回撥函式
const callbackList = this.eventObject[eventName];
if (!callbackList) return console.warn(eventName + " not found!");
// 執行每一個回撥函式
for (let callback of callbackList) {
callback();
}
}
// 訂閱事件
subscribe(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
this.eventObject[eventName] = [];
}
// 儲存訂閱者的回撥函式
this.eventObject[eventName].push(callback);
}
}
// 測試
const eventBus = new EventBus();
// 訂閱事件eventX
eventBus.subscribe("eventX", () => {
console.log("模組A");
});
eventBus.subscribe("eventX", () => {
console.log("模組B");
});
eventBus.subscribe("eventX", () => {
console.log("模組C");
});
// 釋出事件eventX
eventBus.publish("eventX");
// 輸出
> 模組A
> 模組B
> 模組C
上面我們實現了最基礎的釋出和訂閱功能,實際應用中,還可能有更進階的需求。
進階
1. 如何在傳送訊息時傳遞引數
釋出者傳入一個引數到 EventBus
中,在 callback
回撥函式執行的時候接著傳出引數,這樣每一個訂閱者就可以收到引數了。
程式碼
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
}
// 釋出事件
publish(eventName, ...args) {
// 取出當前事件所有的回撥函式
const callbackList = this.eventObject[eventName];
if (!callbackList) return console.warn(eventName + " not found!");
// 執行每一個回撥函式
for (let callback of callbackList) {
// 執行時傳入引數
callback(...args);
}
}
// 訂閱事件
subscribe(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
this.eventObject[eventName] = [];
}
// 儲存訂閱者的回撥函式
this.eventObject[eventName].push(callback);
}
}
// 測試
const eventBus = new EventBus();
// 訂閱事件eventX
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組C", obj, num);
});
// 釋出事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 輸出
> 模組A {msg: 'EventX published!'} 1
> 模組B {msg: 'EventX published!'} 1
> 模組C {msg: 'EventX published!'} 1
2. 訂閱後如何取消訂閱
有時候訂閱者只想在某一個時間段訂閱訊息,這就涉及帶取消訂閱功能。我們將對程式碼進行改造。
首先,要實現指定訂閱者取消訂閱,每一次訂閱事件時,都生成唯一一個取消訂閱的函式,使用者直接呼叫這個函式,我們就把當前訂閱的回撥函式刪除。
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this.eventObject[eventName][id];
};
其次,訂閱的回撥函式列表使換成物件結構儲存,為每一個回撥函式設定一個唯一 id
, 登出回撥函式的時候可以提高刪除的效率,如果還是使用陣列的話需要使用 split
刪除,效率不如物件的 delete
。
程式碼
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
// 回撥函式列表的id
this.callbackId = 0;
}
// 釋出事件
publish(eventName, ...args) {
// 取出當前事件所有的回撥函式
const callbackObject = this.eventObject[eventName];
if (!callbackObject) return console.warn(eventName + " not found!");
// 執行每一個回撥函式
for (let id in callbackObject) {
// 執行時傳入引數
callbackObject[id](...args);
}
}
// 訂閱事件
subscribe(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this.eventObject[eventName] = {};
}
const id = this.callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this.eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this.eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
}
// 測試
const eventBus = new EventBus();
// 訂閱事件eventX
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組B", obj, num);
});
const subscriberC = eventBus.subscribe("eventX", (obj, num) => {
console.log("模組C", obj, num);
});
// 釋出事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 模組C取消訂閱
subscriberC.unSubscribe();
// 再次釋出事件eventX,模組C不會再收到訊息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);
// 輸出
> 模組A {msg: 'EventX published!'} 1
> 模組B {msg: 'EventX published!'} 1
> 模組C {msg: 'EventX published!'} 1
> 模組A {msg: 'EventX published again!'} 2
> 模組B {msg: 'EventX published again!'} 2
3. 如何只訂閱一次
如果一個事件只發生一次,通常也只需要訂閱一次,收到訊息後就不用再接受訊息。
首先,我們提供一個 subscribeOnce
的介面,內部實現幾乎和 subscribe
一樣,只有一個地方有區別,在 callbackId
前面的加一個字元 d
,用來標示這是一個需要刪除的訂閱。
// 標示為只訂閱一次的回撥函式
const id = "d" + this.callbackId++;
然後,在執行回撥函式後判斷當前回撥函式的 id
有沒有標示,決定我們是否需要刪除這個回撥函式。
// 只訂閱一次的回撥函式需要刪除
if (id[0] === "d") {
delete callbackObject[id];
}
程式碼
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
// 回撥函式列表的id
this.callbackId = 0;
}
// 釋出事件
publish(eventName, ...args) {
// 取出當前事件所有的回撥函式
const callbackObject = this.eventObject[eventName];
if (!callbackObject) return console.warn(eventName + " not found!");
// 執行每一個回撥函式
for (let id in callbackObject) {
// 執行時傳入引數
callbackObject[id](...args);
// 只訂閱一次的回撥函式需要刪除
if (id[0] === "d") {
delete callbackObject[id];
}
}
}
// 訂閱事件
subscribe(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this.eventObject[eventName] = {};
}
const id = this.callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this.eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this.eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
// 只訂閱一次
subscribeOnce(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this.eventObject[eventName] = {};
}
// 標示為只訂閱一次的回撥函式
const id = "d" + this.callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this.eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this.eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
}
// 測試
const eventBus = new EventBus();
// 訂閱事件eventX
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組A", obj, num);
});
eventBus.subscribeOnce("eventX", (obj, num) => {
console.log("模組B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組C", obj, num);
});
// 釋出事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 再次釋出事件eventX,模組B只訂閱了一次,不會再收到訊息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);
// 輸出
> 模組A {msg: 'EventX published!'} 1
> 模組C {msg: 'EventX published!'} 1
> 模組B {msg: 'EventX published!'} 1
> 模組A {msg: 'EventX published again!'} 2
> 模組C {msg: 'EventX published again!'} 2
4. 如何清除某個事件或者所有事件
我們還希望通過一個 clear
的操作來將指定事件的所有訂閱清除掉,這個通常在一些元件或者模組解除安裝的時候用到。
// 清除事件
clear(eventName) {
// 未提供事件名稱,預設清除所有事件
if (!eventName) {
this.eventObject = {};
return;
}
// 清除指定事件
delete this.eventObject[eventName];
}
和取消訂閱的邏輯相似,只不過這裡統一處理了。
程式碼
class EventBus {
constructor() {
// 初始化事件列表
this.eventObject = {};
// 回撥函式列表的id
this.callbackId = 0;
}
// 釋出事件
publish(eventName, ...args) {
// 取出當前事件所有的回撥函式
const callbackObject = this.eventObject[eventName];
if (!callbackObject) return console.warn(eventName + " not found!");
// 執行每一個回撥函式
for (let id in callbackObject) {
// 執行時傳入引數
callbackObject[id](...args);
// 只訂閱一次的回撥函式需要刪除
if (id[0] === "d") {
delete callbackObject[id];
}
}
}
// 訂閱事件
subscribe(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this.eventObject[eventName] = {};
}
const id = this.callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this.eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this.eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
// 只訂閱一次
subscribeOnce(eventName, callback) {
// 初始化這個事件
if (!this.eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this.eventObject[eventName] = {};
}
// 標示為只訂閱一次的回撥函式
const id = "d" + this.callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this.eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this.eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this.eventObject[eventName]).length === 0) {
delete this.eventObject[eventName];
}
};
return { unSubscribe };
}
// 清除事件
clear(eventName) {
// 未提供事件名稱,預設清除所有事件
if (!eventName) {
this.eventObject = {};
return;
}
// 清除指定事件
delete this.eventObject[eventName];
}
}
// 測試
const eventBus = new EventBus();
// 訂閱事件eventX
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組A", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組B", obj, num);
});
eventBus.subscribe("eventX", (obj, num) => {
console.log("模組C", obj, num);
});
// 釋出事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 清除
eventBus.clear("eventX");
// 再次釋出事件eventX,由於已經清除,所有模組都不會再收到訊息了
eventBus.publish("eventX", { msg: "EventX published again!" }, 2);
// 輸出
> 模組A {msg: 'EventX published!'} 1
> 模組B {msg: 'EventX published!'} 1
> 模組C {msg: 'EventX published!'} 1
> eventX not found!
5. TypeScript 版本
鑑於現在 TypeScript 已經被大規模採用,尤其是大型前端專案,我們簡要的改造為一個 TypeScript 版本
可以複製以下程式碼到 TypeScript Playground 體驗執行效果
程式碼
interface ICallbackList {
[id: string]: Function;
}
interface IEventObject {
[eventName: string]: ICallbackList;
}
interface ISubscribe {
unSubscribe: () => void;
}
interface IEventBus {
publish<T extends any[]>(eventName: string, ...args: T): void;
subscribe(eventName: string, callback: Function): ISubscribe;
subscribeOnce(eventName: string, callback: Function): ISubscribe;
clear(eventName: string): void;
}
class EventBus implements IEventBus {
private _eventObject: IEventObject;
private _callbackId: number;
constructor() {
// 初始化事件列表
this._eventObject = {};
// 回撥函式列表的id
this._callbackId = 0;
}
// 釋出事件
publish<T extends any[]>(eventName: string, ...args: T): void {
// 取出當前事件所有的回撥函式
const callbackObject = this._eventObject[eventName];
if (!callbackObject) return console.warn(eventName + " not found!");
// 執行每一個回撥函式
for (let id in callbackObject) {
// 執行時傳入引數
callbackObject[id](...args);
// 只訂閱一次的回撥函式需要刪除
if (id[0] === "d") {
delete callbackObject[id];
}
}
}
// 訂閱事件
subscribe(eventName: string, callback: Function): ISubscribe {
// 初始化這個事件
if (!this._eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this._eventObject[eventName] = {};
}
const id = this._callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this._eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this._eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this._eventObject[eventName]).length === 0) {
delete this._eventObject[eventName];
}
};
return { unSubscribe };
}
// 只訂閱一次
subscribeOnce(eventName: string, callback: Function): ISubscribe {
// 初始化這個事件
if (!this._eventObject[eventName]) {
// 使用物件儲存,登出回撥函式的時候提高刪除的效率
this._eventObject[eventName] = {};
}
// 標示為只訂閱一次的回撥函式
const id = "d" + this._callbackId++;
// 儲存訂閱者的回撥函式
// callbackId使用後需要自增,供下一個回撥函式使用
this._eventObject[eventName][id] = callback;
// 每一次訂閱事件,都生成唯一一個取消訂閱的函式
const unSubscribe = () => {
// 清除這個訂閱者的回撥函式
delete this._eventObject[eventName][id];
// 如果這個事件沒有訂閱者了,也把整個事件物件清除
if (Object.keys(this._eventObject[eventName]).length === 0) {
delete this._eventObject[eventName];
}
};
return { unSubscribe };
}
// 清除事件
clear(eventName: string): void {
// 未提供事件名稱,預設清除所有事件
if (!eventName) {
this._eventObject = {};
return;
}
// 清除指定事件
delete this._eventObject[eventName];
}
}
// 測試
interface IObj {
msg: string;
}
type PublishType = [IObj, number];
const eventBus = new EventBus();
// 訂閱事件eventX
eventBus.subscribe("eventX", (obj: IObj, num: number, s: string) => {
console.log("模組A", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
console.log("模組B", obj, num);
});
eventBus.subscribe("eventX", (obj: IObj, num: number) => {
console.log("模組C", obj, num);
});
// 釋出事件eventX
eventBus.publish("eventX", { msg: "EventX published!" }, 1);
// 清除
eventBus.clear("eventX");
// 再次釋出事件eventX,由於已經清除,所有模組都不會再收到訊息了
eventBus.publish<PublishType>("eventX", { msg: "EventX published again!" }, 2);
// 輸出
[LOG]: "模組A", {
"msg": "EventX published!"
}, 1
[LOG]: "模組B", {
"msg": "EventX published!"
}, 1
[LOG]: "模組C", {
"msg": "EventX published!"
}, 1
[WRN]: "eventX not found!"
6. 單例模式
在實際使用過程中,往往只需要一個事件匯流排就能滿足需求,這裡有兩種情況,保持在上層例項中單例和全域性單例。
- 保持在上層例項中單例
將事件匯流排引入到上層例項使用,只需要保證在一個上層例項中只有一個 EventBus
,如果上層例項有多個,意味著有多個事件匯流排,但是每個上層例項管控自己的事件匯流排。
首先在上層例項中建立一個變數用來儲存事件匯流排,只在第一次使用時初始化,後續其他模組使用事件匯流排時直接取得這個事件匯流排例項。
程式碼
// 上層例項
class LWebApp {
private _eventBus?: EventBus;
constructor() {}
public getEventBus() {
// 第一次初始化
if (this._eventBus == undefined) {
this._eventBus = new EventBus();
}
// 後續每次直接取唯一一個例項,保持在LWebApp例項中單例
return this._eventBus;
}
}
// 使用
const eventBus = new LWebApp().getEventBus();
- 全域性單例
有時候我們希望不管哪一個模組想使用我們的事件匯流排,我們都想這些模組使用的是同一個例項,這就是全域性單例,這種設計能更容易統一管理事件。
寫法同上面的類似,區別是要把 _eventBus
和 getEventBus
轉為靜態屬性。使用時無需例項化 EventBusTool
工具類,直接使用靜態方法就行了。
程式碼
// 上層例項
class EventBusTool {
private static _eventBus?: EventBus;
constructor() {}
public static getEventBus(): EventBus {
// 第一次初始化
if (this._eventBus == undefined) {
this._eventBus = new EventBus();
}
// 後續每次直接取唯一一個例項,保持全域性單例
return this._eventBus;
}
}
// 使用
const eventBus = EventBusTool.getEventBus();
原文:https://dushusir.com/js-event...
總結
以上是小編對 Event Bus
的一些理解,基本上實現了想要的效果。通過自己動手實現一遍釋出訂閱模式,也加深了對經典設計模式的理解。其中還有很多不足和需要優化的地方,歡迎大家多多分享自己的經驗。