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:
1// Create emitter
2const emitter = new EventEmitter();
3
4// Subscribe to event
5emitter.subscribe('event1', callback1);
The emit method triggers events:
1emitter.emit('event1', 1, 2);
This passes arguments to listeners.
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!

Try to solve this here or in Interactive Mode.
How do I practice this challenge?
xxxxxxxxxx
class EventEmitter {
subscriptions = new Map();
subscribe(eventName, callback) {
if (!this.subscriptions.has(eventName)) {
this.subscriptions.set(eventName, new Set());
}
const subscriptions = this.subscriptions.get(eventName);
const callbackObj = { callback };
subscriptions.add(callbackObj);
return {
release: () => {
subscriptions.delete(callbackObj);
if (subscriptions.size === 0) {
delete this.subscriptions.eventName;
}
},
};
}
emit(eventName, args) {
const subscriptions = this.subscriptions.get(eventName);
if (subscriptions) {
subscriptions.forEach((cbObj) => {
cbObj.callback.apply(this, args);
});
}
}
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:
- Subscribe to an event with a callback function.
- Unsubscribe from an event to stop receiving notifications.
- Emit an event to notify all current subscribers.

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

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.

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
, anddelete
are generally faster than their object counterparts.
The subscribe
Method: The Heart of the EventEmitter

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:
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:
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:
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

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:
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:
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
andemit
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 arelease
function can make unsubscription efficient. - The
EventEmitter
class, which serves as a custom event-handling system, uses a JavaScriptMap
for thesubscriptions
property to manage event subscriptions optimally, with order preservation and better performance, while the heart of this system, thesubscribe
method, initializes event subscriptions, adds thecallback
to the event's subscriptions, and returns arelease
function to allow unsubscribing, all while ensuring the uniqueness of eachcallback
with aSet
. - The
emit
method in theEventEmitter
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.
xxxxxxxxxx
class EventEmitter {
subscriptions = new Map();
subscribe(eventName, callback) {
if (!this.subscriptions.has(eventName)) {
this.subscriptions.set(eventName, new Set());
}
const subscriptions = this.subscriptions.get(eventName);
const callbackObj = { callback };
subscriptions.add(callbackObj);
return {
release: () => {
subscriptions.delete(callbackObj);
if (subscriptions.size === 0) {
delete this.subscriptions.eventName;
}
},
};
}
emit(eventName, args) {
const subscriptions = this.subscriptions.get(eventName);
if (subscriptions) {
subscriptions.forEach((cbObj) => {
cbObj.callback.apply(this, args);
});
}
}
}
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.