NodeJS Event Emitter

Kinanee Samson - Jul 8 '22 - - Dev Community

Node JS, a JavaScript runtime that is built on top of the V8 JavaScript engine, the same one that powers Google's Chromium browsers. NodeJS has a built in event Emitter which we can inherit in our classes to build an event driven architecture. The API for interacting with the Event Emitter is nothing short of straight forward, we can see the benefit of this style by writing code that will only trigger when a certain event happens, just like the sprinklers turn on when the fire alarm goes off. We are going to explore how to leverage the built in Event Emitter in Node js to write event driven systems, let's dive in.

What is Event Driven Architecture

Event driven architecture is a programming paragdim where the control flow of a program is determined by a sequence of events. There is an event listener that sits and waits for events to occur which it will listen to, we can bind code to the listener thus whenever the event listener subscribes to an event, if a callback function is passed to the listener, the callback code will be called.

Under the hood

In NodeJS there is an event loop which is the name for the data structure that is the Event listener, this allows NodeJs to handle multiple concurrent request while running on a single thread, allowing synchronous code to behave in an asynchronous manner.

Much of the NodeJS core API is built around an idiomatic asynchronous event-driven architecture, note that NodeJS also allows us to spin up multiple child process and worker threads, we are not going to dive into much detail about asynchronous programming in JavaScript but let's get a high level overview of how the event listener works.

All synchronous code gets executed in the call stack, you know in LIFO (Last-in-first-out) manner, event driven code do not follow this manner, the function attached to the event listener is added to the Event's list of internal listeners, when the event is fired, the function attached to the listener is pushed on to the call stack. When the EventEmitter object emits an event, all of the functions attached to that specific event are called synchronously. Any values returned by the called listeners are ignored and discarded. functions in an event's listeners are called in a LIFO (last-in-first-out) manner.

Using the event emitter

To use the event emitter in our code we have to import it from the event emitter module that comes baked in with NodeJS. Our classes can then extend from the event emitter.

import EventEmitter from 'node:events';

Class Alarm extends EventEmitter {};
Enter fullscreen mode Exit fullscreen mode

We now have a starting point, let us discuss some implications of the above code snippet. Our alarm is now a sub class of the EventEmitter and has inherited some interesting methods.

We can now add events to the Alarm, while registering a callback code that will be fired when the event is triggered. We can add events to any Alarm by calling the addEventListener directly on any instance of the Alarm. This function accepts two arguments, a string or property-key which is the name of the event, by which we can identify it and also emit it.

In this process we are registering the callback function along with it's event to the list of internal listeners for that alarm. To fire the call back function we call the emit function passing the name of the event we want to trigger, this will call all the functions registered to that event synchronously.

let FireAlarm = new Alarm();

FireAlarm.addEventListener('smoke', () => {
  console.log('sprinkling water');
})

function triggerAlarm(smoke) {
  if (smoke) {
    FireAlarm.emit('smoke');
  }
}

triggerAlarm(true)

// sprinkling water
Enter fullscreen mode Exit fullscreen mode

We can also accepts parameter when we define the callback function, when we emit the event we pass the arguments right after the event name we are emitting, they will be passed to the event listener function.

let FireAlarm = new Alarm();

FireAlarm.addEventListener('smoke', (threat, Level) => {
  if (threatLevel <= 1) {
    console.log('sprinkling water');
  } else {
   console.log('flooding the building');
  }
})

function triggerAlarm(smoke, threatLevel) {
  if (smoke) {
    FireAlarm.emit('smoke', threatLevel);
  }
}

triggerAlarm(true, 1);
triggerAlarm(true, 2);

// sprinkling water
// flooding the building
Enter fullscreen mode Exit fullscreen mode

There exists an alias for addEventListener, we can use on to register an event. We can also register an event that can only be triggered once using the once Method on the EventEmitter. The emitter will only fire the callback function for this listener at most only once, it is then unregistered from the emitter's internal list of listeners.

let FireAlarm = new Alarm();

FireAlarm.once('fire', () => {
  console.log('on fire');
})

FireAlarm.emit('fire');
// on fire
FireAlarm.emit('fire')
// nothing happens
Enter fullscreen mode Exit fullscreen mode

It is possible to get the events registered on emitter by calling the eventNames Method on the emitter, this will return an array that contains the events that are registered on the emitter.

Let events = FireAlarm.eventNames();

console.log(events);

// ['smoke', 'fire']
Enter fullscreen mode Exit fullscreen mode

We can also set a limit to the amount of listeners we can register to an emitter, if there is a need for this we can call setMaxListeners(n) to cap the number of listeners that can be attached to an instance, to get the maximum number of listeners on an emitter we can call the getMaxListeners . If we don't set the max listener by calling setMaxListener() then getMaxListeners() will return undefined.

We can also get the sum of callback function that are registered to a particular event by calling listernerCount(eventName) which will return an integer.

FireAlarm.setMaxListeners(4);

let smokeListener = FireAlarm.listenerCount('smoke');

console.log(smokeListener);
// 1
console.log(FireAlarm.getMaxListeners())
// 4
Enter fullscreen mode Exit fullscreen mode

If we are more interested in getting the functions that are tied to an event the emitter provides the listerners(eventName) method, when this method is called it returns to us and array of the functions that are registered to that event.

This is quite useful especially when we want to access a particular listener on an emitter, say remove that particular listener. We need the name of the event and the listener that we want to remove, we can then pass them as arguments to emitter.off(eventName, listener) this unsubscribes a listener from an event, it is an alias for removeEventListener()
Which also does the same thing.

// assuming there is no way get the function
// inside here

const fireAlarmListeners = FireAlarm.listeners('smoke')

console.log(fireAlarmListeners);
[ Function ]

FireAlarm.of('smoke', fireAlarmListeners[0])

Enter fullscreen mode Exit fullscreen mode

There are more methods on the event emitter than we can cover here but you can look at the official Nodejs documentation for more information, hope you found this interesting and useful, drop a thing or two you know about Events in Node JS that I left out of this article. Definitely add your thoughts and experience with the event listener.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .