Mark As Completed Discussion

Good morning! Here's our prompt for today.

In modern applications, we often need asynchronous functions. These wait for responses from operations with unknown completion times. So many frameworks use an event-driven architecture.

Certain objects called emitters emit named events. This triggers listener functions to execute.

Each event has an ID. So listeners know which event to subscribe to. If that event fires, the listener is called.

Can you implement a class to enable this? It should:

  • Emit events other code can subscribe to
  • Subscribe to events, getting their outputs

For example:

JAVASCRIPT
1// Create emitter
2const emitter = new EventEmitter(); 
3
4// Subscribe to event
5emitter.subscribe('event1', callback1);

The emit method triggers events:

JAVASCRIPT
1emitter.emit('event1', 1, 2);

This passes arguments to listeners.

JAVASCRIPT
1const emitter = new EventEmitter();
2
3emitter.subscribe('message', (msg) => {
4  console.log('Received message:', msg); 
5});
6
7emitter.emit('message', 'Hello World!');
8
9// Listener outputs:
10// Received message: Hello World!

Question

Try to solve this here or in Interactive Mode.

How do I practice this challenge?

JAVASCRIPT
OUTPUT
:001 > Cmd/Ctrl-Enter to run, Cmd/Ctrl-/ to comment

We'll now take you through what you need to know.

How do I use this guide?

Crafting an EventEmitter

Understanding the Requirements

When we first receive the prompt, it's clear that we needed to create a custom event-handling system. The system should allow users to:

  1. Subscribe to an event with a callback function.
  2. Unsubscribe from an event to stop receiving notifications.
  3. Emit an event to notify all current subscribers.
Crafting an EventEmitter

Given these requirements, we begin thinking about the essential components needed to build this system.

Exploring Examples

Before diving into the code, let's start by exploring some examples to clarify our understanding. Imagine a scenario where multiple components (like buttons or data fetchers) would want to subscribe to an event called 'click'. Each would have its own callback function to execute when a 'click' event occurs.

This exploration leads us to consider a few key questions:

  • How do we keep track of multiple subscribers for multiple events?
  • How do we efficiently notify all subscribers when an event is emitted?

The Brute-Force Approach: The First Draft

The First Draft

Data Structure Choices

Our first instinct for the brute-force approach is to use a simple JavaScript object to keep track of event subscriptions. Each key in the object would correspond to an eventName, and the value would be an array of callback functions.

Functionality

The subscribe method would push the callback function into the array associated with the eventName. The emit method would then loop through this array and execute all the callbacks.

Issues

However, this approach has its limitations:

  • It does not account for duplicate callbacks.
  • Removing a specific callback (unsubscribing) would be inefficient because we'd have to find it in the array and remove it.

Optimizing: Refining the Solution

Given the issues with the brute-force approach, let's look for ways to optimize.

The First Draft

Using a Map

We can use a Map data structure for the subscriptions property to take advantage of its quick lookup and insertion capabilities. Maps also maintain the insertion order, which could be beneficial if the order of events mattered.

Using a Set for Callbacks

Instead of using an array for callbacks, we can use a Set. This would automatically take care of duplicate callbacks, as Sets only store unique values.

Efficient Unsubscription

We also need an efficient way to unsubscribe from events. For this, we can return a release function from the subscribe method. This function would remove the subscriber's callback from the Set, making unsubscription as simple as calling a function.

Solution Implementation

We're tasked with creating an EventEmitter class that can manage various event subscriptions and trigger these events when needed. Essentially, we're building the underpinnings of a custom event-handling system.

Why Use a Map for Subscriptions?

The subscriptions Property

The first thing to note is the subscriptions property, which is a JavaScript Map. A Map in JavaScript holds key-value pairs and remembers the original insertion order of the keys. This makes it an excellent choice for our subscription management because we can store the event names as keys and their respective callbacks as values.

Benefits of Using a Map

  • Order Preservation: Unlike objects, the order of elements is preserved. This could be useful if the order of subscription or event triggering matters.
  • Better Performance: Map operations like get, set, has, and delete are generally faster than their object counterparts.

The subscribe Method: The Heart of the EventEmitter

Solution Implementation

Step 1: Initialize the Event Subscription

When someone wants to subscribe to an event, they'll use the subscribe method, passing in the eventName and a callback function to execute when that event is emitted.

Here's the part of the code that ensures that an entry for this eventName exists in the subscriptions map:

JAVASCRIPT
1if (!this.subscriptions.has(eventName)) {
2    this.subscriptions.set(eventName, new Set());
3}

Why Use a Set for Callbacks?

We use a Set to store all callbacks associated with a particular eventName. The Set ensures that each callback is unique, preventing duplicate callbacks for the same event.

Step 2: Add the Callback

Once we have ensured that an entry exists for the eventName, the next step is to add the callback to this event's subscriptions:

JAVASCRIPT
1const subscriptions = this.subscriptions.get(eventName);
2const callbackObj = { callback };
3subscriptions.add(callbackObj);

We wrap the callback in an object (callbackObj). This gives us more flexibility for future enhancements, like adding additional metadata to the subscription.

Step 3: Return a Release Function

The method returns a release function that allows the subscriber to unsubscribe from the event:

JAVASCRIPT
1return {
2    release: () => {
3        subscriptions.delete(callbackObj);
4        if (subscriptions.size === 0) {
5            delete this.subscriptions.eventName;
6        }
7    },
8};

When the release function is invoked, it removes the callbackObj from the Set of subscriptions. If there are no more subscriptions for this event, it also removes the eventName entry from the subscriptions map.

The emit Method: Triggering the Events

Triggering the Events

Emitting to All Subscribers

The emit method's job is to trigger the event and call all the associated callbacks. It loops through each of the subscriptions and invokes the stored callback functions:

JAVASCRIPT
1emit(eventName, ...args) {
2    const subscriptions = this.subscriptions.get(eventName);
3    if (subscriptions) {
4        subscriptions.forEach((cbObj) => {
5            cbObj.callback.apply(this, args);
6        });
7    }
8}

The ...args syntax allows us to pass any number of arguments to the callback functions. This offers flexibility in what data can be sent when an event is emitted.

The Complete Solution

Putting all these pieces together, we construct our EventEmitter class:

JAVASCRIPT
1class EventEmitter {
2    subscriptions = new Map();
3
4    subscribe(eventName, callback) {
5        if (!this.subscriptions.has(eventName)) {
6            this.subscriptions.set(eventName, new Set());
7        }
8        const subscriptions = this.subscriptions.get(eventName);
9        const callbackObj = { callback };
10        subscriptions.add(callbackObj);
11
12        return {
13            release: () => {
14                subscriptions.delete(callbackObj);
15                if (subscriptions.size === 0) {
16                    delete this.subscriptions.eventName;
17                }
18            },
19        };
20    }
21
22    emit(eventName, ...args) {
23        const subscriptions = this.subscriptions.get(eventName);
24        if (subscriptions) {
25            subscriptions.forEach((cbObj) => {
26                cbObj.callback.apply(this, args);
27            });
28        }
29    }
30}

One Pager Cheat Sheet

  • The document details the need for asynchronous functions and event-driven architecture in modern applications, where emitters emit named events triggering listener functions, while discussing the implementation of a class to emit events and subscribe to them using the subscribe and emit methods respectively.
  • The article discusses how to create a custom event-handling system that allows users to subscribe and unsubscribe from events, and emit events, while considering the efficient management and notification of multiple subscribers for multiple events.
  • The brute-force approach to tracking event subscriptions uses a JavaScript object where each key corresponds to an eventName and the value is an array of callback functions, but this method has issues with duplicate callbacks and inefficient unsubscription; to optimize, a Map data structure can be used for quick lookup and insertion, a Set can be used for callbacks to eliminate duplicates, and a release function can make unsubscription efficient.
  • The EventEmitter class, which serves as a custom event-handling system, uses a JavaScript Map for the subscriptions property to manage event subscriptions optimally, with order preservation and better performance, while the heart of this system, the subscribe method, initializes event subscriptions, adds the callback to the event's subscriptions, and returns a release function to allow unsubscribing, all while ensuring the uniqueness of each callback with a Set.
  • The emit method in the EventEmitter class triggers an event and calls all the associated callbacks, with ...args syntax enabling the passage of any number of arguments to the callback functions.

This is our final solution.

To visualize the solution and step through the below code, click Visualize the Solution on the right-side menu or the VISUALIZE button in Interactive Mode.

JAVASCRIPT
OUTPUT
:001 > Cmd/Ctrl-Enter to run, Cmd/Ctrl-/ to comment

That's all we've got! Let's move on to the next tutorial.

If you had any problems with this tutorial, check out the main forum thread here.