Original text: dushusir.com/js-event-bu…

introduce

An Event Bus is usually used as a communication mechanism between multiple modules. It functions as an Event management center. One module sends messages and the other modules receive messages.

For example, data passing between Vue components can be communicated using an Event Bus, or it can be used as plug-in and core communication in a microkernel plug-in system.

The principle of

Event Bus essentially adopts the publish-subscribe design mode. For example, multiple modules A, B, and C subscribe to an EventX, and then one module X publishes the Event in the Event Bus. Then the Event Bus is responsible for informing all subscribers A, B, and C that they can receive the notification message. You can also pass parameters.

/ / diagramModule X ⬇ release EventX ╔ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╗ ║ Event Bus ║ ║ ║ ║ EventX 】 【 【EventY】 【EventZ】... ║ ╚ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ╝ subscription EventX ⬆ ⬆ subscription EventX ⬆ subscription EventX module module B module CCopy the code

Analysis of the

How do I implement a simple version of the Event Bus using JavaScript

  • First of all, construct aEventBusClass that initializes an empty object to hold all events
  • The event name is used as the key value when the subscription is accepted, and the callback function that needs to be executed after the publication message is accepted is used as the value. Since an event can have multiple subscribers, the callback function here is stored in a list
  • When an event message is released, all the callback functions corresponding to the specified event name are obtained from the event list and triggered in sequence

The following is a detailed implementation of the code, which can be copied to the Google Browser console directly run the detection effect.

code

class EventBus {
  constructor() {
    // Initializes the event list
    this.eventObject = {};
  }
  // Publish events
  publish(eventName) {
    // Retrieve all callback functions for the current event
    const callbackList = this.eventObject[eventName];

    if(! callbackList)return console.warn(eventName + " not found!");

    // Execute each callback function
    for (let callback ofcallbackList) { callback(); }}// Subscribe to events
  subscribe(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // Store the subscriber callback function
    this.eventObject[eventName].push(callback); }}/ / test
const eventBus = new EventBus();

// Subscribe to 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 CCopy the code

Above we have implemented the basic publish and subscribe functionality, but in practice there may be more advanced requirements.

The advanced

1. How do I pass parameters when sending messages

The publisher passes in a parameter to the EventBus, which is then passed out when the callback function is executed so that each subscriber receives the parameter.

code

class EventBus {
  constructor() {
    // Initializes the event list
    this.eventObject = {};
  }
  // Publish events
  publish(eventName, ... args) {
    // Retrieve all callback functions for 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 in the arguments when executing
      callback(...args);
    }
  }
  // Subscribe to events
  subscribe(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      this.eventObject[eventName] = [];
    }

    // Store the subscriber callback function
    this.eventObject[eventName].push(callback); }}/ / test
const eventBus = new EventBus();

// Subscribe to 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
Copy the code

2. How do I unsubscribe after I subscribe

Sometimes subscribers only want to subscribe to messages for a certain period of time, which involves having the ability to unsubscribe. We will modify the code.

First, we implement the specified subscriber to unsubscribe, each subscription event, a unique unsubscribe function is generated, the user directly calls this function, we delete the current subscription callback function.

// For each subscription event, a unique unsubscribe function is generated
const unSubscribe = () = > {
  // Clear the subscriber callback function
  delete this.eventObject[eventName][id];
};
Copy the code

Secondly, the subscribed callback function list should be replaced with object structure storage, and a unique ID should be set for each callback function. When canceling the callback function, the deletion efficiency can be improved. If the array is still used, split deletion should be used, which is less efficient than the object delete.

code

class EventBus {
  constructor() {
    // Initializes the event list
    this.eventObject = {};
    // The id of the callback function list
    this.callbackId = 0;
  }
  // Publish events
  publish(eventName, ... args) {
    // Retrieve all callback functions for 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 in the arguments when executing
      callbackObject[id](...args);
    }
  }
  // Subscribe to events
  subscribe(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this.eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this.eventObject[eventName][id];

      // If the event has no subscribers, 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 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 unsubscribe
subscriberC.unSubscribe();

// Publish eventX again, module C will no longer receive messages
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
Copy the code

3. How to subscribe only once

If an event happens only once, it usually only needs to be subscribed once, and once the message is received, it does not need to be received again.

First of all, we provide an interface for subscribeOnce. The internal implementation is almost the same as subscribe, except that a character D is added before the callbackId to indicate that this is a subscription that needs to be deleted.

// marked as a callback that subscribes only once
const id = "d" + this.callbackId++;
Copy the code

Then, after executing the callback function, determine if the id of the current callback function is marked and if we need to remove the callback function.

// Subscribe once callback needs to be removed
if (id[0= = ="d") {
  delete callbackObject[id];
}
Copy the code

code

class EventBus {
  constructor() {
    // Initializes the event list
    this.eventObject = {};
    // The id of the callback function list
    this.callbackId = 0;
  }
  // Publish events
  publish(eventName, ... args) {
    // Retrieve all callback functions for 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 in the arguments when executingcallbackObject[id](... args);// Subscribe once callback needs to be removed
      if (id[0= = ="d") {
        deletecallbackObject[id]; }}}// Subscribe to events
  subscribe(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this.eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this.eventObject[eventName][id];

      // If the event has no subscribers, clear the entire event object
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName]; }};return { unSubscribe };
  }

  // Subscribe only once
  subscribeOnce(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this.eventObject[eventName] = {};
    }

    // marked as a callback that subscribes only once
    const id = "d" + this.callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this.eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this.eventObject[eventName][id];

      // If the event has no subscribers, 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 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 eventX again. Module B subscribed once and no longer receives 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
Copy the code

4. How do I clear an event or all events

We also want to clear all subscriptions to a given event with a clear operation, which is usually used when some component or module is uninstalled.

  // Clear the event
  clear(eventName) {
    // No event name is provided. All events are cleared by default
    if(! eventName) {this.eventObject = {};
      return;
    }

    // Clear the specified event
    delete this.eventObject[eventName];
  }
Copy the code

The logic is similar to unsubscribe, except that it is handled uniformly.

code

class EventBus {
  constructor() {
    // Initializes the event list
    this.eventObject = {};
    // The id of the callback function list
    this.callbackId = 0;
  }
  // Publish events
  publish(eventName, ... args) {
    // Retrieve all callback functions for 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 in the arguments when executingcallbackObject[id](... args);// Subscribe once callback needs to be removed
      if (id[0= = ="d") {
        deletecallbackObject[id]; }}}// Subscribe to events
  subscribe(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this.eventObject[eventName] = {};
    }

    const id = this.callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this.eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this.eventObject[eventName][id];

      // If the event has no subscribers, clear the entire event object
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName]; }};return { unSubscribe };
  }

  // Subscribe only once
  subscribeOnce(eventName, callback) {
    // Initialize the event
    if (!this.eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this.eventObject[eventName] = {};
    }

    // marked as a callback that subscribes only once
    const id = "d" + this.callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this.eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this.eventObject[eventName][id];

      // If the event has no subscribers, clear the entire event object
      if (Object.keys(this.eventObject[eventName]).length === 0) {
        delete this.eventObject[eventName]; }};return { unSubscribe };
  }

  // Clear the event
  clear(eventName) {
    // 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 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);

/ / remove
eventBus.clear("eventX");

// Publish eventX again, since it has been cleared, all modules will no longer receive messages
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!

Copy the code

5. The TypeScript version

Given the widespread adoption of TypeScript, especially on large front-end projects, we’ll briefly change to a version of TypeScript

You can copy the following code to the TypeScript Playground to experience how it works

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() {
    // Initializes the event list
    this._eventObject = {};
    // The id of the callback function list
    this._callbackId = 0;
  }
  // Publish events
  publish<T extends any[]>(eventName: string. args: T):void {
    // Retrieve all callback functions for 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 in the arguments when executingcallbackObject[id](... args);// Subscribe once callback needs to be removed
      if (id[0= = ="d") {
        deletecallbackObject[id]; }}}// Subscribe to events
  subscribe(eventName: string.callback: Function): ISubscribe {
    // Initialize the event
    if (!this._eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this._eventObject[eventName] = {};
    }

    const id = this._callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this._eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this._eventObject[eventName][id];

      // If the event has no subscribers, clear the entire event object
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName]; }};return { unSubscribe };
  }

  // Subscribe only once
  subscribeOnce(eventName: string.callback: Function): ISubscribe {
    // Initialize the event
    if (!this._eventObject[eventName]) {
      // With object storage, cancel the callback function to improve the efficiency of deletion
      this._eventObject[eventName] = {};
    }

    // marked as a callback that subscribes only once
    const id = "d" + this._callbackId++;

    // Store the subscriber callback function
    // The callbackId needs to be incremented for the next callback function
    this._eventObject[eventName][id] = callback;

    // For each subscription event, a unique unsubscribe function is generated
    const unSubscribe = () = > {
      // Clear the subscriber callback function
      delete this._eventObject[eventName][id];

      // If the event has no subscribers, clear the entire event object
      if (Object.keys(this._eventObject[eventName]).length === 0) {
        delete this._eventObject[eventName]; }};return { unSubscribe };
  }

  // Clear the event
  clear(eventName: string) :void {
    // 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 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);

/ / remove
eventBus.clear("eventX");

// Publish eventX again, since it has been cleared, all modules will no longer receive messages
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!"
Copy the code

6. Singleton mode

In practice, only one event bus is often required to meet the requirements, and there are two cases, singletons and global singletons that remain in the upper instance.

  1. Keep singletons in upper-level instances

To introduce the EventBus to the upper-layer instance, you only need to ensure that there is only one EventBus in one 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 EventBus. A variable is created in the upper instance to store the event bus, which is initialized only when it is used for the first time. The event bus instance is acquired directly when other modules use the event bus.

code

// Upper-level instance
class LWebApp {
  private_eventBus? : EventBus;constructor() {}

  public getEventBus() {
    // Initialization for the first time
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // Take a single instance each time and keep it in the LWebApp instance
    return this._eventBus; }}/ / use
const eventBus = new LWebApp().getEventBus();
Copy the code
  1. Global singleton

Sometimes we want no matter which module wants to use our event bus, we want those modules to use the same instance. This is a global singleton. This design makes it easier to manage events uniformly.

Write the same as above, except that _eventBus and getEventBus are static. Use static methods instead of instantiating the EventBusTool tool class.

code

// Upper-level instance
class EventBusTool {
  private static_eventBus? : EventBus;constructor() {}

  public static getEventBus(): EventBus {
    // Initialization for the first time
    if (this._eventBus == undefined) {
      this._eventBus = new EventBus();
    }

    // Take a unique instance each time to keep the global singleton
    return this._eventBus; }}/ / use
const eventBus = EventBusTool.getEventBus();
Copy the code

Original text: dushusir.com/js-event-bu…

conclusion

The above is the small editor’s understanding of the Event Bus, which basically achieves the desired effect. By implementing the publish/subscribe pattern yourself, you also deepen your understanding of classic design patterns. There are still many deficiencies and need to be optimized, welcome to share their experience.

reference

  • How do I implement Event Bus in JavaScript
  • How to Implement an Event Bus in TypeScript
  • Implement EventBus with JS
  • Vue EventBus (Vue EventBus