Editor’s note: This post was updated 13 September 2022 to include information about why one should use event emitters, the class members within EventEmitter
, and make other general updates to the text.
Events are actions that have software or hardware significance. They are emitted due to either user activities, such as a mouse click or keystrokes, or directly from systems, such as errors or notifications.
The JavaScript language enables us to respond to events by running code within an event handler. Since Node.js is based on JavaScript, it leverages this feature in its event-driven architecture.
In this article, we’ll discuss what event emitters are and why we should use them, as well as how to build custom event emitters. Let’s get started!
EventEmitter
Events in Node.js are actions that occur in our application that we can respond to. There are two different types of events in Node.js, both of which we’ll cover in this article:
System events occur from the C++ side of the Node.js core and are handled by a C library used in Node.js called libuv.
The libuv library deals with lower-level events that are happening inside of the computer system, essentially, low-level events that come from the OS, such as receiving data from the internet, finished reading a file, and so on.
Because the flow of a Node.js program is determined by events (event-driven programming), all I/O requests would eventually generate a completion/failure event.
Side note: JavaScript does not have built-in capabilities to handle system events.
Custom events occur inside the JavaScript core in Node.js. These events are handled by a Node.js class called EventEmitter
.
EventEmitter
is a JavaScript class in Node.js implemented to handle custom events because JavaScript does not have any built-in capability to handle events otherwise.
Often, when an event occurs in libuv, libuv generates custom JavaScript events so they can be easily handled. Consequently, Node.js events might be seen as a single entity even though they are actually two different types of events.
In this article, our focus will be on custom JavaScript events and the event emitter.
Node.js uses the event-driven programming pattern as noted above. This design pattern is for producing and consuming events and is predominantly used in the frontend (browser). User actions such as a button click or a keypress generate an event.
These events have associated functions called listeners, which are called to handle their corresponding events when they are emitted.
The event-driven pattern has two main components:
Node.js brings event-driven programming to the backend through the EventEmitter
class. The EventEmitter
class is a part of the events
core Node.js module. It provides a way for us to emit and handle events, and it exposes — among many others — the on
and the emit
methods.
In this article, we will build a custom event emitter by using the core principles that powers Node.js event emitters so that we can better understand how they work.
The code below shows how to emit and handle an event using the EventEmitter
object.
const EventEmitter = require('events'); const eventEmitter = new EventEmitter (); eventEmitter.on('greet', () => { console.log('Hello world!'); }); eventEmitter.emit('greet');
In the code above, the eventEmitter.on
method is used to register listeners that handle an event, while the eventEmitter.emit
method is used to emit events.
By emitting the greet
event above, the function that listens for the greet
event is called, and Hello world!
is logged to the console.
Other methods exposed by the EventEmitter
objects are:
eventEmitter.once()
: This adds a one-time listenereventEmitter.off()
: This is an alias for eventEmitter.removeListener()
. It removes an event listener from an eventeventEmitter.listnersCount()
: This returns the number of listeners listening to an eventIn Node.js, there are other objects that can emit events. However, all objects that emit events are an instance of the EventEmitter
class. To best understand the event emitters, let’s build our own.
Node.js brings event-driven programming to the backend through the EventEmitter
class. The EventEmitter
class is a part of the events
core Node.js module.
Event emitters and listeners are crucial to Node.js development. They enable the Node.js runtime to accomplish its single-threaded, asynchronous, nonblocking I/O feature that is the crux of Node.js’ awesome performance.
EventEmitter
The Node.js EventEmitter
class provides a way for us to emit and handle events. Within it, it exposes many class members:
emit
method sequentially and synchronously calls each of the listeners listening for the fired event — passing the supplied argument to eachon
method takes two arguments: the event name and an event listener. It adds this event listener function to the end of the array of listenersoff
is an alias for the emitter.removeListener()
removeListener
method takes two arguments: the event name and an event listener. Then it removes this event listener from the array of events when the specified event firesaddListener
is an alias for the on
methodThe general idea of an event emitter is to have an event object whose keys act as custom events, and the corresponding values would be arrays of listeners that are invoked synchronously when the event is emitted.
Consider the code below:
const GreetHandlers = [ ()=> {console.log("Hello world!"), ()=> {console.log("Hello Developer!"), ()=> {console.log("Hello From LogRocket!"} ]; const events = { "greet": GreetHandlers }
Here, the events
object has one property: "greet"
. Every property name of this object is considered a unique event, meaning that the "greet"
property is an event.
The value of the "greet"
property (event) is the GreetHandlers
array. This is an array containing functions (or listeners) that would be called synchronously when the "greet"
event occurs.
To call these functions synchronously, we loop through the array and invoke each function, as seen below:
GreetHandlers.forEach(listener => { listener(); }); // Output: // "Hello world!" // "Hello Developer!" // "Hello From LogRocket!"
Our example above gives a simplified overview of the pattern that is used in the Node.js event emitter. We will employ the same pattern as we build our own event emitter in the next section.
Although the Node.js EventEmitter
is a JavaScript class, we will be building our own using a function constructor so that we can understand what is happening in the background.
Classes in JavaScript give us a new and easy syntax to work with the JavaScript’s prototypal inheritance. Because classes in JavaScript are syntactic sugar for the prototypal pattern, many things occur under the hood that is hidden from us.
To build our custom event emitter, follow the steps below:
This should have one property (the event object).
function Emitter( ) { this.events = { }; }
The events
object above would serve as the main object that holds all custom events. Each key
and value
of this object corresponds to an event and its array of listeners.
function Emitter( ) { this.events = { "greet": [ ()=> {}, ()=> {}, ()=> {}, ], "speak": [ ()=> {}, ()=> {}, ()=> {}, ] }
Object-oriented JavaScript gives us a clean way to share properties and methods across our app. This is because the prototypal pattern allows objects to access properties down the prototype chain, meaning an object can access properties in its prototype, in the prototype of its prototype, and beyond.
The JavaScript engine first searches for a method or property in the prototype of an object if it is absent in the object. If it does not find it in the prototype of that object, it continues its search down the prototype chain. This pattern of inheritance is known as prototypal inheritance.
Because of JavaScript’s prototypal inheritance, when we add properties and method to an object’s prototype, all instances of that object would have access to them.
See here in the code below, where we add the on
method:
Emitter.prototype.on = function (type, listener) { // check if the listener is a function and throw error if it is not if(typeof listener !== "function") { throw new Error("Listener must be a function!") } // create the event listener property (array) if it does not exist. this.events[type] = this.events[type] || []; // adds listners to the events array. this.events[type].push(listener); }
This adds the on
function to the prototype of the Emitter
object, which allows all instances of the Emitter
object to inherit this method. The on
method takes two arguments, namely type
and listener
(a function).
First, the on
method checks if the listener
is a function. If it is not, it throws an error as seen below:
if(typeof listener !== "function") { throw new Error("Listener must be a function!") }
Also, the on
method checks if the type
of event is present in the events
object (as a key). If it isn’t present, it adds an event (as a key) to the events
object and sets its value to an empty array. Finally, it adds listeners to the corresponding event array: this.events[type].push(listener);
.
Now let’s add the emit
method:
Emitter.prototype.emit = function(type) { if (this.events[type]) { // checks if event is a property on Emitter this.events[type].forEach(function(listener) { // loop through that events array and invoke all the listeners inside it. listener( ); }) } } // if used as a node module. Exports this function constructor modules.exports = Emitter; // This makes it available from the require() // so we can make as many instances of it as we want.
The code above adds the emit
method to the Emitters
prototype. It simply checks if the event type is present (as a key) in the events
object. If it is present, it then invokes all the corresponding listeners as already discussed above.
this.events[type]
returns the value of the corresponding event property, which is an array containing listeners.
Consequently, the code below loops through the array and invokes all its listeners synchronously.
this.events[type].forEach(function(listener) { // loop through that events array and invoke all the listeners inside it. listener( ); })
To use our event emitter, we would have to manually emit an event.
// if used in a Node.js environment require the module as seen below. const Emitter = require('./emitter'); const eventEmitter = new Emitter(); eventEmitter.on('greet', ()=> { console.log("Hello World!"); }); eventEmitter.on('greet', ()=> { console.log("Hello from LogRocket!"); }); eventEmitter.emit("greet"); // manually emit an event
In the code above, we first require and create an instance of the Emitter
module using this:
const Emitter = require('./emitter'); const eventEmitter = new Emitter();
Then we assign listeners to the "greet"
event using the on
method.
eventEmitter.on('greet', ()=> { console.log("Hello World!"); }); eventEmitter.on('greet', ()=> { console.log("Hello from LogRocket!"); });
Finally, we manually emit the greet
event using this line:
emtr.emit("greet");
The image above shows the result of running our code. View the full code in a sandbox here.
addListener
methodIt is important to note that the on
method is actually an alias for the addListener
method in the event emitter. Thus, we need to refactor out implementation.
Emitter.prototype.addListener = function (type, listener) { // check if the listener is a function and throw error if it is not if (typeof listener !== "function") { throw new Error("Listener must be a function!"); } // create the event listener property (array) if it does not exist. this.events[type] = this.events[type] || []; // adds listners to the events array. this.events[type].push(listener); }; Emitter.prototype.on = function (type, listener) { return this.addListener(type, listener); };
The above code still works, but in this version, both the addListener
and the on
method do the same thing.
listenersCount
methodThere’s also the listenersCount
method. This returns the total number of functions (listeners) that are listening for a particular event. We will implement this below:
Emitter.prototype.listenerCount = function (type) { let listnersCount = 0; let listeners = this.events[type] || []; listnersCount = listners.length; console.log("listeners listnersCount", listnersCount); return listnersCount; };
Here, we tell the simple store the array of listeners for the specified event to the listeners
variable using:
let listeners = this.events[type] || [];
If the event is not found, an empty array is stored. Then, we return the length
of the listener variable
.
Event emitters in Node.js follow this same idea, but they have a lot of extra features. We just built a simple version. You can play with final code in plain JavaScript here.
The event emitter is a fundamental building block of many parts of the Node.js JavaScript core. All Node.js objects that emit events, such as streams and the HTTP module, are instances of the EventEmitter
class. It is an important object in Node.js that is defined and exposed by the events
module.
Through the EventEmitter
class, Node.js brings event-driven programming to the server side. I hope by building our contrived event emitter that you have been able to learn more about the Node.js EventEmitter
class.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to implement one-way and two-way data binding in Vue.js, using v-model and advanced techniques like defineModel for better apps.
Compare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
One Reply to "How to build custom Node.js event emitters"
Nice post! Very clear and well explained. Thanks