[Medium] LeetCode JS 30 - Event Emitter

March 3, 2024

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee

LeetCode 30 Days of JavaScript

This question is from LeetCode's 30 Days of JavaScript Challenge

Question Prompt

Design an EventEmitter class. This interface is similar (but with some differences) to the one found in Node.js or the Event Target interface of the DOM. The EventEmitter should allow for subscribing to events and emitting them.

Your EventEmitter class should have the following two methods:

  • subscribe - This method takes in two arguments: the name of an event as a string and a callback function. This callback function will later be called when the event is emitted. An event should be able to have multiple listeners for the same event. When emitting an event with multiple callbacks, each should be called in the order in which they were subscribed. An array of results should be returned. You can assume no callbacks passed to subscribe are referentially identical.The subscribe method should also return an object with an unsubscribe method that enables the user to unsubscribe. When it is called, the callback should be removed from the list of subscriptions and undefined should be returned.
  • emit - This method takes in two arguments: the name of an event as a string and an optional array of arguments that will be passed to the callback(s). If there are no callbacks subscribed to the given event, return an empty array. Otherwise, return an array of the results of all callback calls in the order they were subscribed.

Example 1:

Input:
actions = ["EventEmitter", "emit", "subscribe", "subscribe", "emit"],
values = [[], ["firstEvent", "function cb1() { return 5; }"],  ["firstEvent", "function cb1() { return 6; }"], ["firstEvent"]]

Output: [[],["emitted",[]],["subscribed"],["subscribed"],["emitted",[5,6]]]

Explanation:
const emitter = new EventEmitter();
emitter.emit("firstEvent"); // [], no callback are subscribed yet
emitter.subscribe("firstEvent", function cb1() { return 5; });
emitter.subscribe("firstEvent", function cb2() { return 6; });
emitter.emit("firstEvent"); // [5, 6], returns the output of cb1 and cb2

Solutions

First, create a EventEmitter class that acts as the central hub for managing event subscriptions and emissions. The constructor function initializes a this.events, which is a Map. A Map is chosen because it efficiently associates event names (strings) with a list of callback functions that care about that event.

For the subscribe method, first check if there are already any events for the given eventName. If not, create a new entry in the this.events Map with the event name as the key and an empty array as the value.

The provided callback function is pushed onto the array of events for that event name. Then, return a unsubscribe method, which returns an object containing an unsubscribe function. This is essential for later removing a listener if we don't need it anymore.

For the unsubscribe method, first search for the callback within the array of events for the event using indexOf. If the callback is found (index !== -1), use splice to remove that callback from the array of events.

Then, we move on to the emit method. This method will first check if the eventName is not in the this.events, return an empty array.

Then, we create a result array, and then get the array of callback functions associated with the eventName. Then, iterate over the array, calling each callback function with the provided arguments (args). The result of each callback is collected in the result array.

Finally, return the result array containing the return values from each of the callbacks.

class EventEmitter {
  constructor() {
    this.events = new Map();
  }

  subscribe(eventName, callback) {
    if (!this.events.has(eventName)) {
      this.events.set(eventName, []);
    }

    const events = this.events.get(eventName);
    events.push(callback);

    return {
      unsubscribe: () => {
        const index = events.indexOf(callback);
        if (index !== -1) {
          events.splice(index, 1);
        }
      },
    };
  }

  emit(eventName, args = []) {
    if (!this.events.has(eventName)) {
      return [];
    }

    const result = [];
    const events = this.events.get(eventName);

    events.forEach((callback) => {
      result.push(callback(...args));
    });

    return result;
  }
}

☕️ Support Us
Your support will help us to continue to provide quality content.👉 Buy Me a Coffee