If you are learning JavaScript, you have probably come across things
like events, callbacks, arrays, and DOM manipulation separately.
I recently worked through a small notification system that brought all
of these concepts together. At first, the code looked a little
complicated, especially the EventEmitter part. But once I broke it
down, the whole idea became much easier to understand.
This blog explains the same system in a beginner-friendly way.
What are we trying to build?
Imagine a social media application.
Three things happen:
- Alex follows you
- Sam likes your post
- John comments on your post
We want to show notifications like:
Alex followed you
Sam liked your post
John commented on your post
The important part is that these notifications should appear one after
another, not all at once.
That gives us two main problems to solve:
- How do we know when an event happens?
- How do we display notifications in the correct order?
The solution is:
EventEmitter + Notification Queue
1. Understanding Events
An event is simply something that happens.
For example:
Someone followed me
Someone liked my post
Someone commented on my post
We can give these events names:
"new-follower"
"like"
"comment"
Instead of directly connecting every part of our application together,
we can say:
"When this event happens, run this function."
This is where an EventEmitter becomes useful.
2. Creating the EventEmitter
Here is the basic structure:
class EventEmitter {
constructor() {
this.events = {};
}
}
A class is basically a blueprint.
When we write:
const notificationEmitter = new EventEmitter();
we create an actual EventEmitter object.
The constructor runs automatically and creates:
this.events = {};
At this point, events is just an empty object:
{}
We will use it to store our event listeners.
3. What is this?
If this is your first time working with classes, this can be
confusing.
When we create:
const notificationEmitter = new EventEmitter();
this refers to the object being created.
So:
this.events = {};
is essentially giving our new object an events property.
Conceptually:
notificationEmitter.events = {};
So our object starts like this:
notificationEmitter
|
└── events
|
└── {}
4. Listening to an Event with on()
Now we need a way to listen for events.
That's what the on() method does:
on(eventName, callback) {
if (!this.events[eventName]) {
this.events[eventName] = [];
}
this.events[eventName].push(callback);
}
For example:
emitter.on("like", handleLike);
This means:
"When the
likeevent happens, rememberhandleLike."
Here:
"like"
is the event name.
And:
handleLike
is the callback.
5. What is a Callback?
A callback is simply a function that we give to another function so it
can be called later.
For example:
function handleLike() {
console.log("Someone liked the post");
}
emitter.on("like", handleLike);
We don't write:
emitter.on("like", handleLike());
because that would execute the function immediately.
Instead:
emitter.on("like", handleLike);
means:
"Here is the function. Run it when the
likeevent occurs."
6. Why do we use an Array?
Inside on() we have:
if (!this.events[eventName]) {
this.events[eventName] = [];
}
Suppose we listen for "like" for the first time.
Initially:
this.events = {};
There is no "like" property.
So we create:
this.events["like"] = [];
Now we can add callbacks:
this.events["like"].push(callback);
Eventually, the object could look like:
{
like: [functionA, functionB],
comment: [functionC],
"new-follower": [functionD]
}
The array is useful because multiple functions can listen to the same
event.
7. Stopping a Listener with off()
Sometimes we don't want a function to listen anymore.
That's what off() does:
off(eventName, callback) {
if (!this.events[eventName]) {
return;
}
this.events[eventName] =
this.events[eventName].filter(
(listener) => listener !== callback
);
}
Suppose we have:
emitter.on("like", handleLike);
Later, we can remove it:
emitter.off("like", handleLike);
The filter() method creates a new array that keeps every listener
except the one we want to remove.
For example:
Before:
[functionA, functionB, functionC]
Remove functionB
After:
[functionA, functionC]
So the easy way to remember it is:
on → start listening
off → stop listening
8. Triggering an Event with emit()
Now comes the most important part.
emit(eventName, data) {
if (!this.events[eventName]) {
return;
}
this.events[eventName].forEach((callback) => {
callback(data);
});
}
emit() means:
"This event happened."
For example:
emitter.emit("like");
means:
"A like happened!"
We can also send information with the event:
emitter.emit("like", {
name: "Sam"
});
Now the callback receives:
{
name: "Sam"
}
as its data.
9. How emit() Finds the Right Function
Suppose we registered:
emitter.on("like", (data) => {
console.log(`${data.name} liked your post`);
});
Then we trigger:
emitter.emit("like", {
name: "Sam"
});
The EventEmitter looks inside:
this.events["like"]
and finds the callback.
Then:
forEach()
goes through the listeners.
Finally:
callback(data);
runs the callback and passes the data to it.
The result is:
Sam liked your post
10. Connecting the EventEmitter to Notifications
Now we can connect the EventEmitter to our notification system.
For a new follower:
notificationEmitter.on(
"new-follower",
(data) => {
addNotification(
`${data.name} followed you`
);
}
);
For a like:
notificationEmitter.on(
"like",
(data) => {
addNotification(
`${data.name} liked your post`
);
}
);
For a comment:
notificationEmitter.on(
"comment",
(data) => {
addNotification(
`${data.name} commented on your post`
);
}
);
Now the EventEmitter doesn't need to know how notifications are
displayed.
It simply triggers the appropriate callback.
The callback creates the notification message.
11. The Notification Queue
Now we have another problem.
What happens if three notifications arrive at almost the same time?
We don't want:
Alex followed you
Sam liked your post
John commented
to all fight for the same space on the screen.
Instead, we use a queue:
const notificationQueue = [];
A queue works like a line.
The first notification that enters should be the first one displayed.
This is called:
FIFO --- First In, First Out
For example:
Alex
Sam
John
Alex goes first, then Sam, then John.
12. Adding Notifications to the Queue
Our function is:
function addNotification(message) {
notificationQueue.push(message);
showNextNotification();
}
The first line:
notificationQueue.push(message);
adds the notification to the end of the queue.
For example:
[]
Then:
["Alex followed you"]
Then:
[
"Alex followed you",
"Sam liked your post"
]
And finally:
[
"Alex followed you",
"Sam liked your post",
"John commented"
]
So:
push()
means:
Add something to the end.
13. Taking the First Notification
When it's time to display a notification, we use:
const message = notificationQueue.shift();
shift() removes the first item.
For example:
[
"Alex followed you",
"Sam liked your post",
"John commented"
]
After shift():
message = "Alex followed you"
And the queue becomes:
[
"Sam liked your post",
"John commented"
]
This is what gives us FIFO behavior.
Remember:
push() → add to the end
shift() → remove from the beginning
14. Making Sure Only One Notification Shows
We use a simple flag:
let isShowingNotification = false;
This tells us whether a notification is currently visible.
When nothing is showing:
false
When a notification is on screen:
true
This prevents multiple notifications from appearing at the same time.
15. The Important if Statement
Inside showNextNotification() we have:
if (
isShowingNotification ||
notificationQueue.length === 0
) {
return;
}
This basically asks two questions:
- Is a notification already showing?
- Is the queue empty?
If either answer is yes, we stop.
The || means OR.
So in plain English:
"If something is already showing OR there is nothing waiting in the
queue, don't do anything."
This small check is what keeps the queue under control.
16. Creating the Notification in the DOM
Once we're ready to display a notification:
const notification = document.createElement("div");
This creates a new <div> using JavaScript.
Then:
notification.className = "notification-toast";
gives it a CSS class.
And:
notification.textContent = message;
puts the notification message inside it.
Finally:
container.appendChild(notification);
adds it to the webpage.
If the HTML contains:
<div id="notification-container"></div>
we can end up with:
<div id="notification-container">
<div class="notification-toast">
Sam liked your post
</div>
</div>
17. Removing the Notification
We don't want the notification to stay forever.
So we use:
setTimeout(() => {
notification.remove();
isShowingNotification = false;
showNextNotification();
}, 3000);
The 3000 means 3000 milliseconds, or 3 seconds.
So the process is:
Show notification
↓
Wait 3 seconds
↓
Remove notification
↓
Set isShowingNotification to false
↓
Show the next notification
This is what makes the notifications appear one after another.
18. Putting Everything Together
Now the complete flow makes much more sense.
Suppose we run:
notificationEmitter.emit(
"new-follower",
{
name: "Alex Developer"
}
);
The flow is:
emit()
↓
Find "new-follower" listener
↓
Run callback
↓
Create "Alex Developer followed you"
↓
addNotification()
↓
push() into queue
↓
showNextNotification()
↓
Display notification
Then:
notificationEmitter.emit(
"like",
{
name: "Sam"
}
);
creates:
Sam liked your post
If Alex's notification is still visible, Sam's notification waits in the
queue.
Then the comment event creates:
John commented on your post
That also waits.
The final queue might look like:
[
"Sam liked your post",
"John commented on your post"
]
Once Alex's notification disappears, Sam appears.
Then John appears.
19. The Full Mental Model
The easiest way I found to understand the project is to think of it as
two systems working together.
EventEmitter
Something happens
↓
emit()
↓
Find listeners
↓
Run callback
Notification Queue
Callback creates message
↓
push()
↓
Wait in queue
↓
shift()
↓
Display notification
↓
Wait 3 seconds
↓
Remove notification
↓
Show next one
Together:
User action
↓
Event
↓
emit()
↓
EventEmitter
↓
callback
↓
addNotification()
↓
Queue
↓
showNextNotification()
↓
DOM
↓
3 seconds
↓
Remove
↓
Next notification
20. What I Learned from This
The biggest lesson for me was that the code looks much more complicated
when everything is viewed at once.
Breaking it into smaller ideas makes it easier:
-
classcreates the EventEmitter blueprint. -
on()stores listeners. -
off()removes listeners. -
emit()triggers listeners. -
callbackis the function that runs when an event occurs. -
push()adds a notification to the queue. -
shift()takes the oldest notification out. -
setTimeout()controls how long the notification stays visible. - DOM methods create and remove the notification on the page.
Once these pieces are understood individually, the whole notification
system becomes much easier to follow.
Final Takeaway
You don't need to memorize the entire code.
If you're learning this from scratch, focus on these five ideas first:
on() → listen
emit() → trigger
callback → respond
push() → add to queue
shift() → take from queue
That's the foundation of this notification system.
And honestly, that's one of the useful things about building small
JavaScript projects: concepts that seem abstract in isolation start
making sense when you see them working together.
Top comments (0)