How to Implement an Event Bus in JavaScript
openHacking
Posted on March 10, 2022
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
Reference
Posted on March 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.