Things that make you far better Node.js developer, part3 (events & event emitter)

Hossein Derakhshan
6 min readJan 15, 2023

--

In this article, I want to talk about events and event emitter, which is not limited to node.js, and you can find it literally everywhere. I will create an event emitter from scratch and I will discuss how that can help.

What is Event?

Event is something that has happened, and we might want to respond to.
In Node.js we have two kind of events:

  • System events
  • custom events

System events are something that manages in c++ part of Node.js in a module named libuv. I will talk about libuv in upcoming articles but basically, what it does is it listens to the events that happen in the Operation system like accepting a request from the internet or finishing reading a file or anything that is happening in the operation system. since it is manged in c++ part of Node.js, there is a need to have a bridge between this part and Node.js and this is where custom events help us.

Custom events are written in the Javascript part of Node.js namedEventEmitter. We create custom events when we want to know what code should run when something happens. Libuv also uses custom events to make system events accessible in javascript.

Cool, now Let’s create a simpleEventEmitter from scratch with a practical example.

Let’s assume we want to create a function that is supposed to calculate a time-consuming job. we want to know how much progress this function had so far. let’s create a file named heavyCalculation.js and put these codes there:


class heavyCalculation {
calculate(pulse) {
const max = 999999999;
let invoke1 ,invoke2, invoked3;

for (let index = 0; index < max; index++) {
if (!invoke1 && index > (max * 25 / 100)) {
invoke1 = true
pulse('20%', index)
} else if (!invoke2 && index > (max * 50 / 100)) {
invoke2 = true
pulse('50%', index)
} else if (!invoked3 && index > (max * 75 / 100)) {
invoked3 = true
pulse('75%', index)
} else if (index +1 === max) {
pulse('100%', index)
}
}
}
}

I created a class named heavyCalculation and this class has a method named calculate . this method has an argument named pulse.

pulse Should be a function (Thanks for First-class feature of JS if you don’t know what it is you can read this), and we want to use it as a callback.

Now let’s run calculate function:

// initialize the class
const obj = new heavyCalculation()

const pulse = (progress, index) => {
console.log(`we had ${progress} progress so far and the index is now ${index}`)
}

obj.calculate(pulse)

if you run this code, the result would be:

we achieved what we wanted. now pulse function is a kind of event. so what is the problem here? let’s assume in another file if progress is 75%, we want to do something like send an email. one solution is we can edit pulse function to be something like this:

const pulse = (progress, index) => {
if (progress === '75%') {
sendEmail()
}
console.log(`we had ${progress} progress so far and the index is now ${index}`)
}

this solution works, but what is the problem:

it violates Open–closed principle but what does that mean in a simple word:

Open-closed is one principle in SOLID. SOLID is a set of standards that help us to write codes that are resilient for upcoming changes. Open-closed principle simply says, try to minimize any changes in the the module that is works. because you might change something that has some negative impact on another part of the code.

let’s assume the product owner comes, and says, we also want to send an SMS when the progress is 75%. we can edit pulse function and call another function for sending SMS:

const pulse = (progress, index) => {
if (progress === '75%') {
sendSMS()
sendEmail()
}
console.log(`we had ${progress} progress so far and the index is now ${index}`)
}

as you can see, we are changing this file by any requests to change. during these changes, we might make a mistake, and that mistake might disrupt the other functionality of this file that had worked before.

Another problem that we might face is, as time goes by, this method becomes a big file because all the application logic is here, and this approach creates many more side effects, eg it makes if difficult to write a test for this function because you might need to mock many class or function here…

now that we discussed the downside of our approach, let’s talk about how events can help us to fix these problems.

every event contains two parts which are the name of the event and the listener.

Listener simply is a function like pulse that we already saw.

let’s create a class name Emitter:

class Emitter {
constructor() {
this.events = {}
}

on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(listener)
}
}

this class has a method named on that has two parameters. eventName and listener . let’s run this function to see what it does:

const obj = new Emitter();
const pulse = (progress, index) => {
console.log(`we had ${progress} progress so far and the index is now ${index}`)
}
obj.on('progress', pulse)

when we call on , first, it checked whether this eventName exists in the this.events or not. if it does not exist, it assigns an empty array for that name, and then it pushes the provided listener in it. the reason that it pushes into an array is because we can push many listener for that.

this.events now is like this.

{
progress: [pulse]
}

if we call another on it pushes another listener eg:

const sendEmail = (progress, index) => {
// send email
}
obj.on('progress', sendEmail)

now this.events is like this:

{
progress: [pulse, sendEmail]
}

cool, so far, we stored all the listeners in this.events. now we need a function to call these listeners when we want. we call this function emit

so let’s add emit function into our Emitter class

class Emitter {
constructor() {
this.events = {}
}

on(eventName, listener) {
if (!this.events[eventName]) {
this.events[eventName] = []
}
this.events[eventName].push(listener)
}

emit(eventName, payload) {
if (this.events[eventName]) {
this.events[eventName].forEach((listener) => {
listener(payload);
})
}
}
}

if now we call emit it invokes all the listeners that are registered for that particular eventName

obj.emit('progress', {progress: '25%', index: 1000})

basically, emit function checks whether this eventName is exist or not. if it exists, it calls all the functions(listeners) that are in the array and passes them payload as an argument.

Now it’s time to refactor our previous code with the event implementation

class HeavyCalculation extends Emitter{
calculate() {
const max = 999999999;
let invoke1 ,invoke2, invoked3;

for (let index = 0; index < max; index++) {
if (!invoke1 && index > (max * 25 / 100)) {
invoke1 = true
this.emit('update', {progress: '20%', index} )
} else if (!invoke2 && index > (max * 50 / 100)) {
invoke2 = true
this.emit('update', {progress: '50%', index} )
} else if (!invoked3 && index > (max * 75 / 100)) {
invoked3 = true
this.emit('update', {progress: '75%', index} )
} else if (index +1 === max) {
this.emit('update', {progress: '100%', index} )
}
}
}
}

let’s analyze this class, first HeavyCalculation inherits from the Emitter meaning we can have access to the all the methods of Emitter in HeavyCalculation class.

As you can see now calculate function doesn’t have any arguments. I removed pulse function from the code and instead of calling the pulse function, I called this.emit which is a method in Emitter

let’s initialize and call calculate function:
calculate.js :

var obj = new HeavyCalculation()
obj.calculate()
module.exports = obj

now since obj inherits from Emitter, I can call on function and listen to any events that might happen whenever and wherever I want.

for example, in email module, I can say:

email.js

const calculate = require('./calculate')

const sendEmail(payload) {

}

calculate.on('update', sendEmail)

as you can see now, we only put the logic of the email in this listener. so it fixed the issues that we discussed guys.

f you check the events.js file in node.js source code, it used the advanced version of this code with many validations and … but the idea is the same.

There are many node.js modules that are built in on top of Emitter and you will see a lot of on and emit everywhere in node.js

That’s it guys!

see you in the next article which I want to talk about buffer, streams and libuv.

Love you :)

--

--

Hossein Derakhshan

Hey, I'm Hossein. I'm a javascript developer who wants to know what is going on behind the scenes of these tools :)