Introduction
The Event Bus
is usually used as a communication mechanism between multiple modules, which is equivalent to an event management center. One module sends messages, and other modules receive messages, which achieves the function of communication.
For example, data passing between Vue components can be communicated using an Event Bus
, or it can be used as plugin and core communication in the microkernel plugin system.
Principle
Event Bus
essentially adopts a publish-subscribe design pattern. For example, multiple modules A
, B
, and C
subscribe to an event EventX
, and then a certain module X
publishes this event on the event bus, then the event bus will be responsible for notifying all subscriptions. A
, B
, C
, they can all receive this notification message, and can also pass parameters.
// relation chart module X ⬇ Release EventX ╔════════════════════════════════════════════════════════════════════╗ ║ Event Bus ║ ║ ║ ║ 【EventX】 【EventY】 【EventZ】 ... ║ ╚════════════════════════════════════════════════════════════════════╝ ⬆Subscribe to EventX ⬆Subscribe to EventX ⬆Subscribe to EventX Module A Module B Module C
Analysis
How to implement a simple version of Event Bus
using JavaScript
- First construct an
EventBus
class, initialize an empty object to store all events - When accepting a subscription, use the event name as the key value, and use the callback function that needs to be executed after accepting the published message as the value. Since an event may have multiple subscribers, the callback function here should be stored as a list
- When publishing an event message, get all the callback functions corresponding to the specified event name from the event list, and trigger and execute them in sequence
The following is the detailed implementation of the code, which can be copied to the Google Chrome console to run the detection effect directly.
Code
class EventBus { constructor() { // initialize event list this.eventObject = {}; } // publish event publish(eventName) { // Get all the callback functions of the current event const callbackList = this.eventObject[eventName]; if (!callbackList) return console.warn(eventName + " not found!"); // execute each callback function for (let callback of callbackList) { callback(); } } // Subscribe to events subscribe(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { this.eventObject[eventName] = []; } // store the callback function of the subscriber this.eventObject[eventName].push(callback); } } // test const eventBus = new EventBus(); // Subscribe to event eventX eventBus.subscribe("eventX", () => { console.log("Module A"); }); eventBus.subscribe("eventX", () => { console.log("Module B"); }); eventBus.subscribe("eventX", () => { console.log("Module C"); }); // publish event eventX eventBus.publish("eventX"); // output > Module A > Module B > Module C
We have implemented the most basic publish and subscribe functions above. In practical applications, there may be more advanced requirements.
Advanced
1. How to pass parameters when sending a message
The publisher passes a parameter into EventBus
, and then passes the parameter when the callback
function is executed, so that each subscriber can receive the parameter.
Code
class EventBus { constructor() { // initialize event list this.eventObject = {}; } // publish event publish(eventName, ...args) { // Get all the callback functions of the current event const callbackList = this.eventObject[eventName]; if (!callbackList) return console.warn(eventName + " not found!"); // execute each callback function for (let callback of callbackList) { // pass parameters when executing callback(...args); } } // Subscribe to events subscribe(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { this.eventObject[eventName] = []; } // store the callback function of the subscriber this.eventObject[eventName].push(callback); } } // test const eventBus = new EventBus(); // Subscribe to event eventX eventBus.subscribe("eventX", (obj, num) => { console.log("Module A", obj, num); }); eventBus.subscribe("eventX", (obj, num) => { console.log("Module B", obj, num); }); eventBus.subscribe("eventX", (obj, num) => { console.log("Module C", obj, num); }); // publish event eventX eventBus.publish("eventX", { msg: "EventX published!" }, 1); // output > Module A {msg: 'EventX published!'} 1 > Module B {msg: 'EventX published!'} 1 > Module C {msg: 'EventX published!'} 1
2. How to unsubscribe after subscription
Sometimes subscribers only want to subscribe to messages in a certain period of time, which involves the ability to unsubscribe. We will revamp the code.
First of all, to achieve the specified subscriber unsubscribe, each time an event is subscribed, a unique unsubscribe function is generated. The user directly calls this function, and we delete the currently subscribed callback function.
// Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this.eventObject[eventName][id]; };
Secondly, the subscribed callback function list is stored in an object structure, and a unique id
is set for each callback function. When canceling the callback function, the efficiency of deletion can be improved. If you still use an array, you need to use split
to delete, which is less efficient than delete
of objects.
Code
class EventBus { constructor() { // initialize event list this.eventObject = {}; // id of the callback function list this.callbackId = 0; } // publish event publish(eventName, ...args) { // Get all the callback functions of the current event const callbackObject = this.eventObject[eventName]; if (!callbackObject) return console.warn(eventName + " not found!"); // execute each callback function for (let id in callbackObject) { // pass parameters when executing callbackObject[id](...args); } } // Subscribe to events subscribe(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this.eventObject[eventName] = {}; } const id = this.callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this.eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this.eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this.eventObject[eventName]).length === 0) { delete this.eventObject[eventName]; } }; return { unSubscribe }; } } // test const eventBus = new EventBus(); // Subscribe to event eventX eventBus.subscribe("eventX", (obj, num) => { console.log("Module A", obj, num); }); eventBus.subscribe("eventX", (obj, num) => { console.log("Module B", obj, num); }); const subscriberC = eventBus.subscribe("eventX", (obj, num) => { console.log("Module C", obj, num); }); // publish event eventX eventBus.publish("eventX", { msg: "EventX published!" }, 1); // Module C unsubscribes subscriberC.unSubscribe(); // Publish the event eventX again, module C will no longer receive the message eventBus.publish("eventX", { msg: "EventX published again!" }, 2); // output > Module A {msg: 'EventX published!'} 1 > Module B {msg: 'EventX published!'} 1 > Module C {msg: 'EventX published!'} 1 > Module A {msg: 'EventX published again!'} 2 > Module B {msg: 'EventX published again!'} 2
3. How to subscribe only once
If an event occurs only once, it usually only needs to be subscribed once, and there is no need to receive messages after receiving messages.
First, we provide an interface of subscribeOnce
, the internal implementation is almost the same as subscribe
, there is only one difference, add a character d
before callbackId
to indicate that this is a subscription that needs to be deleted.
// Callback function marked as subscribe only once const id = "d" + this.callbackId++;
Then, after executing the callback function, judge whether the id
of the current callback function is marked, and decide whether we need to delete the callback function.
// The callback function that is only subscribed once needs to be deleted if (id[0] === "d") { delete callbackObject[id]; }
Code
class EventBus { constructor() { // initialize event list this.eventObject = {}; // id of the callback function list this.callbackId = 0; } // publish event publish(eventName, ...args) { // Get all the callback functions of the current event const callbackObject = this.eventObject[eventName]; if (!callbackObject) return console.warn(eventName + " not found!"); // execute each callback function for (let id in callbackObject) { // pass parameters when executing callbackObject[id](...args); // The callback function that is only subscribed once needs to be deleted if (id[0] === "d") { delete callbackObject[id]; } } } // Subscribe to events subscribe(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this.eventObject[eventName] = {}; } const id = this.callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this.eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this.eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this.eventObject[eventName]).length === 0) { delete this.eventObject[eventName]; } }; return { unSubscribe }; } // only subscribe once subscribeOnce(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this.eventObject[eventName] = {}; } // Callback function marked as subscribe only once const id = "d" + this.callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this.eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this.eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this.eventObject[eventName]).length === 0) { delete this.eventObject[eventName]; } }; return { unSubscribe }; } } // test const eventBus = new EventBus(); // Subscribe to event eventX eventBus.subscribe("eventX", (obj, num) => { console.log("Module A", obj, num); }); eventBus.subscribeOnce("eventX", (obj, num) => { console.log("Module B", obj, num); }); eventBus.subscribe("eventX", (obj, num) => { console.log("Module C", obj, num); }); // publish event eventX eventBus.publish("eventX", { msg: "EventX published!" }, 1); // Publish the event eventX again, module B only subscribes once, and will not receive any more messages eventBus.publish("eventX", { msg: "EventX published again!" }, 2); // output > Module A {msg: 'EventX published!'} 1 > Module C {msg: 'EventX published!'} 1 > Module B {msg: 'EventX published!'} 1 > Module A {msg: 'EventX published again!'} 2 > Module C {msg: 'EventX published again!'} 2
4. How to clear an event or all events
We also hope to clear all subscriptions of the specified event through a clear
operation, which is usually used when some components or modules are uninstalled.
// clear event clear(eventName){ // If no event name is provided, all events are cleared by default if(!eventName){ this.eventObject = {} return } // clear the specified event delete this.eventObject[eventName] }
Similar to the logic of unsubscribing, except that it is handled uniformly here.
Code
class EventBus { constructor() { // initialize event list this.eventObject = {}; // id of the callback function list this.callbackId = 0; } // publish event publish(eventName, ...args) { // Get all the callback functions of the current event const callbackObject = this.eventObject[eventName]; if (!callbackObject) return console.warn(eventName + " not found!"); // execute each callback function for (let id in callbackObject) { // pass parameters when executing callbackObject[id](...args); // The callback function that is only subscribed once needs to be deleted if (id[0] === "d") { delete callbackObject[id]; } } } // Subscribe to events subscribe(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this.eventObject[eventName] = {}; } const id = this.callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this.eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this.eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this.eventObject[eventName]).length === 0) { delete this.eventObject[eventName]; } }; return { unSubscribe }; } // only subscribe once subscribeOnce(eventName, callback) { // initialize this event if (!this.eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this.eventObject[eventName] = {}; } // Callback function marked as subscribe only once const id = "d" + this.callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this.eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this.eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this.eventObject[eventName]).length === 0) { delete this.eventObject[eventName]; } }; return { unSubscribe }; } // clear event clear(eventName) { // If no event name is provided, all events are cleared by default if (!eventName) { this.eventObject = {}; return; } // clear the specified event delete this.eventObject[eventName]; } } // test const eventBus = new EventBus(); // Subscribe to event eventX eventBus.subscribe("eventX", (obj, num) => { console.log("Module A", obj, num); }); eventBus.subscribe("eventX", (obj, num) => { console.log("Module B", obj, num); }); eventBus.subscribe("eventX", (obj, num) => { console.log("Module C", obj, num); }); // publish event eventX eventBus.publish("eventX", { msg: "EventX published!" }, 1); // clear eventBus.clear("eventX"); // Publish the event eventX again, since it has been cleared, all modules will no longer receive the message eventBus.publish("eventX", { msg: "EventX published again!" }, 2); // output > Module A {msg: 'EventX published!'} 1 > Module B {msg: 'EventX published!'} 1 > Module C {msg: 'EventX published!'} 1 > eventX not found!
5. TypeScript version
TypeScript is now widely adopted, especially for large front-end projects, we briefly revamp it to a TypeScript version
You can copy the following code to TypeScript Playground to run
Code
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() { // initialize event list this._eventObject = {}; // id of the callback function list this._callbackId = 0; } // publish event publish<T extends any[]>(eventName: string, ...args: T): void { // Get all the callback functions of the current event const callbackObject = this._eventObject[eventName]; if (!callbackObject) return console.warn(eventName + " not found!"); // execute each callback function for (let id in callbackObject) { // pass parameters when executing callbackObject[id](...args); // The callback function that is only subscribed once needs to be deleted if (id[0] === "d") { delete callbackObject[id]; } } } // Subscribe to events subscribe(eventName: string, callback: Function): ISubscribe { // initialize this event if (!this._eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this._eventObject[eventName] = {}; } const id = this._callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this._eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this._eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this._eventObject[eventName]).length === 0) { delete this._eventObject[eventName]; } }; return { unSubscribe }; } // only subscribe once subscribeOnce(eventName: string, callback: Function): ISubscribe { // initialize this event if (!this._eventObject[eventName]) { // Use object storage to improve the efficiency of deletion when logging out the callback function this._eventObject[eventName] = {}; } // Callback function marked as subscribe only once const id = "d" + this._callbackId++; // store the callback function of the subscriber // callbackId needs to be incremented after use for the next callback function this._eventObject[eventName][id] = callback; // Every time you subscribe to an event, a unique unsubscribe function is generated const unSubscribe = () => { // clear the callback function of this subscriber delete this._eventObject[eventName][id]; // If this event has no subscribers, also clear the entire event object if (Object.keys(this._eventObject[eventName]).length === 0) { delete this._eventObject[eventName]; } }; return { unSubscribe }; } // clear event clear(eventName: string): void { // If no event name is provided, all events are cleared by default if (!eventName) { this._eventObject = {}; return; } // clear the specified event delete this._eventObject[eventName]; } } // test interface IObj { msg: string; } type PublishType = [IObj, number]; const eventBus = new EventBus(); // Subscribe to event eventX eventBus.subscribe("eventX", (obj: IObj, num: number, s: string) => { console.log("Module A", obj, num); }); eventBus.subscribe("eventX", (obj: IObj, num: number) => { console.log("Module B", obj, num); }); eventBus.subscribe("eventX", (obj: IObj, num: number) => { console.log("Module C", obj, num); }); // publish event eventX eventBus.publish("eventX", { msg: "EventX published!" }, 1); // clear eventBus.clear("eventX"); // Publish the event eventX again, since it has been cleared, all modules will no longer receive the message eventBus.publish<PublishType>("eventX", { msg: "EventX published again!" }, 2); // output [LOG]: "Module A", { "msg": "EventX published!" }, 1 [LOG]: "Module B", { "msg": "EventX published!" }, 1 [LOG]: "Module C", { "msg": "EventX published!" }, 1 [WRN]: "eventX not found!"
6. Singleton Pattern
In actual use, only one event bus is often needed to meet the requirements. There are two cases here, keep the singleton in the upper instance, and the global singleton.
- Keep the singleton in the upper instance
Import the event bus to the upper-layer instance, it only needs to ensure that there is only one EventBus
in an upper-layer instance. If there are multiple upper-layer instances, it means that there are multiple event buses, but each upper-layer instance controls its own event bus.
First, a variable is established in the upper-level instance to store the event bus, which is only initialized when it is used for the first time, and the event bus instance is directly obtained when other modules use the event bus.
Code
// upper instance class LWebApp { private _eventBus?: EventBus; constructor() {} public getEventBus() { // first initialization if (this._eventBus == undefined) { this._eventBus = new EventBus(); } // Subsequent to directly take only one instance each time, keep it as a single instance in the LWebApp instance return this._eventBus; } } // use const eventBus = new LWebApp().getEventBus();
- Global singleton
Sometimes we hope that no matter which module wants to use our event bus, we all want these modules to use the same instance, which is a global singleton. This design makes it easier to manage events in a unified manner.
The writing method is similar to the above, the difference is to convert _eventBus
and getEventBus
to static properties. There is no need to instantiate the EventBusTool
class when using it, just use the static method directly.
Code
// upper instance class EventBusTool { private static _eventBus?: EventBus; constructor() {} public static getEventBus(): EventBus { // first initialization if (this._eventBus == undefined) { this._eventBus = new EventBus(); } // Subsequent to directly take a unique instance each time, keep the global singleton return this._eventBus; } } // use const eventBus = EventBusTool.getEventBus();
Conclusion
The above are some of the my understanding of Event Bus
, which basically achieves the desired feature. By implementing the publish-subscribe model by yourself, it also deepens the understanding of the classic design pattern. There are still many shortcomings and areas that need to be optimized. Welcome to share your experience.
Check my blog https://lwebapp.com/en/posts
Top comments (0)