如何在 JavaScript 中實現 Event Bus(事件匯流排)

Dushusir發表於2022-03-13

原文:https://dushusir.com/js-event...

介紹

Event Bus 事件匯流排,通常作為多個模組間的通訊機制,相當於一個事件管理中心,一個模組傳送訊息,其它模組接受訊息,就達到了通訊的作用。

比如,Vue 元件間的資料傳遞可以使用一個 Event Bus 來通訊,也可以用作微核心外掛系統中的外掛和核心通訊。

原理

Event Bus 本質上是採用了釋出-訂閱的設計模式,比如多個模組 ABC 訂閱了一個事件 EventX,然後某一個模組 X 在事件匯流排釋出了這個事件,那麼事件匯流排會負責通知所有訂閱者 ABC,它們都能收到這個通知訊息,同時還可以傳遞引數。

// 關係圖
                           模組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. 單例模式

在實際使用過程中,往往只需要一個事件匯流排就能滿足需求,這裡有兩種情況,保持在上層例項中單例和全域性單例。

  1. 保持在上層例項中單例

將事件匯流排引入到上層例項使用,只需要保證在一個上層例項中只有一個 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();
  1. 全域性單例

有時候我們希望不管哪一個模組想使用我們的事件匯流排,我們都想這些模組使用的是同一個例項,這就是全域性單例,這種設計能更容易統一管理事件。

寫法同上面的類似,區別是要把 _eventBusgetEventBus 轉為靜態屬性。使用時無需例項化 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 的一些理解,基本上實現了想要的效果。通過自己動手實現一遍釋出訂閱模式,也加深了對經典設計模式的理解。其中還有很多不足和需要優化的地方,歡迎大家多多分享自己的經驗。

參考

相關文章