How to Implement an Event Bus in JavaScript

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 XRelease 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) => {
  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<PublishType>("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.

  1. 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();
  1. 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.

Reference

Comments