But without further ado, let’s get to the topic under discussion: “Event Emitters”. Event Emitters play a very important role in the Node.js ecosystem.
The EventEmitter is a module that facilitates communication/interaction between objects in Node. EventEmitter is at the core of Node asynchronous event-driven architecture. Many of Node’s built-in modules inherit from EventEmitter including prominent frameworks like Express.js.
The concept is quite simple: emitter objects emit named events that cause previously registered listeners to be called. So, an emitter object basically has two main features:
It’s kind of like a pub/sub or observer design pattern (though not exactly).
The above basic features are sufficient to implement a full system using the eventing model.
Before we get into the coding, let’s take a look at how we will be using the EventEmitter class. Please note that our code will mimic the exact API of the Node.js ‘events’ module.
In fact, if you replace our EventEmitter with Node.js’s built-in ‘events’ module you will get the same result.
const myEmitter = new EventEmitter();
function c1() {
console.log('an event occurred!');
}
function c2() {
console.log('yet another event occurred!');
}
myEmitter.on('eventOne', c1); // Register for eventOne
myEmitter.on('eventOne', c2); // Register for eventOne
When the event ‘eventOne’ is emitted, both the above callbacks should be invoked.
myEmitter.emit('eventOne');
The output in the console will be as follows:
an event occurred!
yet another event occurred!
myEmitter.once('eventOnce', () => console.log('eventOnce once fired'));
Emitting the event ‘eventOnce’:
myEmitter.emit('eventOne');
The following output should appear in the console:
eventOnce once fired
Emitting events registered with once again will have no impact.
myEmitter.emit('eventOne');
Since the event was only emitted once, the above statement will have no impact.
myEmitter.on('status', (code, msg)=> console.log(`Got ${code} and ${msg}`));
Emitting the event with parameters:
myEmitter.emit('status', 200, 'ok');
The output in the console will be as below:
Got 200 and ok
NOTE: You can emit events multiple times (except the ones registered with the once method).
myEmitter.off('eventOne', c1);
Now if you emit the event as follows, nothing will happen and it will be a noop:
myEmitter.emit('eventOne'); // noop
console.log(myEmitter.listenerCount('eventOne'));
NOTE: If the event has been unregistered using off or removeListener method, then the count will be 0.
console.log(myEmitter.rawListeners('eventOne'));
// Example 2->Adapted and thanks to Sameer Buna
class WithTime extends EventEmitter {
execute(asyncFunc, ...args) {
this.emit('begin');
console.time('execute');
this.on('data', (data)=> console.log('got data ', data));
asyncFunc(...args, (err, data) => {
if (err) {
return this.emit('error', err);
}
this.emit('data', data);
console.timeEnd('execute');
this.emit('end');
});
}
}
Using the withTime event emitter:
const withTime = new WithTime();
withTime.on('begin', () => console.log('About to execute'));
withTime.on('end', () => console.log('Done with execute'));
const readFile = (url, cb) => {
fetch(url)
.then((resp) => resp.json()) // Transform the data into json
.then(function(data) {
cb(null, data);
});
}
withTime.execute(readFile, 'https://jsonplaceholder.typicode.com/posts/1');
Check the output in the console. The list of posts will be displayed along with other logs.
Since we now understand the usage API, let’s get to coding the module.
We will be filling in the details incrementally in the next couple sections.
class EventEmitter {
listeners = {}; // key-value pair
addListener(eventName, fn) {}
on(eventName, fn) {}
removeListener(eventName, fn) {}
off(eventName, fn) {}
once(eventName, fn) {}
emit(eventName, ...args) { }
listenerCount(eventName) {}
rawListeners(eventName) {}
}
We begin by creating the template for the EventEmitter class along with a hash to store the listeners. The listeners will be stored as a key-value pair. The value could be an array (since for the same event we allow multiple listeners to be registered).
Let us now implement the addListener method. It takes in an event name and a callback function to be executed.
addListener(event, fn) {
this.listeners[event] = this.listeners[event] || [];
this.listeners[event].push(fn);
return this;
}
The addListener event checks if the event is already registered. If yes, returns the array, otherwise empty array.
this.listeners[event] // will return array of events or undefined (first time registration)
For example…
Let’s understand this with a usage example. Let’s create a new eventEmitter and register a ‘test-event’. This is the first time the ‘test-event’ is being registered.
const eventEmitter = new EventEmitter();
eventEmitter.addListener('test-event',
()=> { console.log ("test one") }
);
Inside addListener () method:
this.listeners[event] => this.listeners['test-event']
=> undefined || []
=> []
The result will be:
this.listeners['test-event'] = []; // empty array
and then the ‘fn’ will be pushed to this array as shown below:
this.listeners['test-event'].push(fn);
I hope this makes the ‘addListener’ method very clear to decipher and understand.
A note: Multiple callbacks can be registered against that same event.
This is just an alias to the ‘addListener’ method. We will be using the ‘on’ method more than the ‘addListener’ method for the sake of convenience.
on(event, fn) {
return this.addListener(event, fn);
}
The removeListener method takes an eventName and the callback as the parameters. It removes said listener from the event array.
NOTE: If the event has multiple listeners then other listeners will not be impacted.
First, let’s take a look at the full code for removeListener.
removeListener (event, fn) {
let lis = this.listeners[event];
if (!lis) return this;
for(let i = lis.length; i > 0; i--) {
if (lis[i] === fn) {
lis.splice(i,1);
break;
}
}
return this;
}
Here’s the removeListener method explained step-by-step:
This is just an alias to the ‘removeListener’ method. We will be using the ‘on’ method more than the ‘addListener’ method for sake of convenience.
off(event, fn) {
return this.removeListener(event, fn);
}
Adds a one-time listener
function for the event named eventName
. The next time eventName
is triggered, this listener is removed and then invoked.
Use for setup/init kind of events.
Let’s take a peek at the code.
once(eventName, fn) {
this.listeners[event] = this.listeners[eventName] || [];
const onceWrapper = () => {
fn();
this.off(eventName, onceWrapper);
}
this.listeners[eventName].push(onceWrapper);
return this;
}
Here’s the once method explained step-by-step:
Synchronously calls each of the listeners registered for the event named eventName
, in the order they were registered, passing the supplied arguments to each.
Returns true
if the event had listeners, false
otherwise.
emit(eventName, ...args) {
let fns = this.listeners[eventName];
if (!fns) return false;
fns.forEach((f) => {
f(...args);
});
return true;
}
Here’s the emit method explained step-by-step:
Returns the number of listeners listening to the event named eventName
.
Here’s the source code:
listenerCount(eventName) {
let fns = this.listeners[eventName] || [];
return fns.length;
}
Here’s the listenerCount method explained step-by-step:
Returns a copy of the array of listeners for the event named eventName
, including any wrappers (such as those created by .once()
). The once wrappers in this implementation will not be available if the event has been emitted once.
rawListeners(event) {
return this.listeners[event];
}
The full source code for reference:
class EventEmitter {
listeners = {}
addListener(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
this.listeners[eventName].push(fn);
return this;
}
on(eventName, fn) {
return this.addListener(eventName, fn);
}
once(eventName, fn) {
this.listeners[eventName] = this.listeners[eventName] || [];
const onceWrapper = () => {
fn();
this.off(eventName, onceWrapper);
}
this.listeners[eventName].push(onceWrapper);
return this;
}
off(eventName, fn) {
return this.removeListener(eventName, fn);
}
removeListener (eventName, fn) {
let lis = this.listeners[eventName];
if (!lis) return this;
for(let i = lis.length; i > 0; i--) {
if (lis[i] === fn) {
lis.splice(i,1);
break;
}
}
return this;
}
emit(eventName, ...args) {
let fns = this.listeners[eventName];
if (!fns) return false;
fns.forEach((f) => {
f(...args);
});
return true;
}
listenerCount(eventName) {
let fns = this.listeners[eventName] || [];
return fns.length;
}
rawListeners(eventName) {
return this.listeners[eventName];
}
}
The complete code is available here:
https://jsbin.com/gibofab/edit?js,console,output
As an exercise feel free to implement other events’ APIs from the documentation https://nodejs.org/api/events.html.
If you liked this article and want to see more of similar articles, feel free to give a couple of claps :)
NOTE: The code is optimized for readability and not for performance. Maybe as an exercise, you can optimize the code and share it in the comment section. Haven’t tested fully for edge cases and some validations may be off as this was a quick writeup.
This article is part of the upcoming video course “Node.JS Master Class — Build Your Own ExpressJS-Like MVC Framework from scratch”.
Recommended Courses:
☞ Master the MEAN Stack - Learn By Example
☞ ChatBots: Messenger ChatBot with API.AI and Node.JS
☞ Learn Node.js API’s Fast and Simple
☞ A Simple Node.js/Mongo/Restify API in Less Than 3 Hours
☞ JavaScript Programming Tutorial Full Course for Beginners
☞ Learn JavaScript - Become a Zero to Hero
☞ Javascript Project Tutorial: Budget App
☞ E-Commerce JavaScript Tutorial - Shopping Cart from Scratch