Node.js: Sending and receiving events with EventEmitter

EventEmitter is one of the core idioms of Node.js. If Node.js’s core idea is an event- driven architecture, emitting events from an object is one of the primary mechanisms of that architecture. EventEmitter is an object that gives notifications (events) at different points in its life cycle. For example, an HTTPServer object emits events concerning each stage of the startup/shutdown of the Server object and at each stage of processing HTTP requests from HTTP clients.

Many core Node.js modules are EventEmitter objects, and EventEmitter objects are an excellent skeleton on which to implement asynchronous programming.

EventEmitter objects are so much a part of the Node.js woodwork that you may skip over their existence. However, because they’re used everywhere, we need some understanding of what they are and how to use them when necessary.

In this chapter, we’ll work with the HTTPServer and HTTPClient objects. Both are subclasses of the EventEmitter class and rely on it to send events for each step of the HTTP protocol. In this section, we’ll first learn about using JavaScript classes, and then we will create an EventEmitter subclass so that we can learn about EventEmitter.

1. JavaScript classes and class inheritance

Before getting started on the EventEmitter class, we need to take a look at another one of the ES2015 features: classes. JavaScript has always had objects and the concept of a class hierarchy, but nothing as formal as in other languages. The ES2015 class object builds on the existing prototype-based inheritance model, but with a syntax that looks a lot like class definitions in other languages.

For example, consider the following class, which we’ll be using later in this book:

class Note {

constructor(key, title, body) {

this._key = key;

this._title = title;

this._body = body;

}

get key() { return this._key; }

get title() { return this._title; }

set title(newTitle) { return this._title = newTitle; }

get body() { return this._body; }

set body(newBody) { return this._body = newBody; }

}

This should look familiar to anyone who’s implemented a class definition in other languages. The class has a name—Note. There is also a constructor method and attributes for each instance of the class.

Once you’ve defined the class, you can export the class definition to other modules:

module.exports.Note = class Note { .. }       # in CommonJS modules

export class Note { .. }                      # in ES6 modules

Functions marked with the get or set keywords are getters and setters, used as follows:

const aNote = new Note(“key”, “The Rain in Spain”, “Falls mainly on the plain”);

const key = aNote.key;

var title = aNote.title;

aNote.title = “The Rain in Spain, which made me want to cry with joy”;

New instances of a class are created with new. You access a getter or setter function as if it is a simple field on the object. Behind the scenes, the getter/setter function is invoked.

The preceding implementation is not the best because the _title and _body fields are publicly visible and there is no data-hiding or encapsulation. There is a technique to better hide the field data, which we’ll go over in Chapter 5, Your First Express Application.

You can test whether a given object is of a certain class by using the instanceof operator:

if (anotherNote instanceof Note) {

… it’s a Note, so act on it as a Note

}

Finally, you declare a subclass using the extends operator, similar to how you would in other languages:

class LoveNote extends Note {

constructor(key, title, body, heart) {

super(key, title, body);

this._heart = heart;

}

get heart() { return this._heart; }

set heart(newHeart) { return this._heart = newHeart; }

}

In other words, the LoveNote class has all the fields of Note, plus a new field named heart.

This was a brief introduction to JavaScript classes. By the end of this book, you’ll have had lots of practice with this feature. The EventEmitter class gives us a practical use for classes and class inheritance.

2. The EventEmitter class

The EventEmitter object is defined in the events module of Node.js. Using the EventEmitter class directly means performing require(‘events’). In most cases, we don’t do this. Instead, our typical use of EventEmitter objects is via an existing object that uses EventEmitter internally. However, there are some cases where needs dictate implementing an EventEmitter subclass.

Create a file named pulser.mjs, containing the following code:

import EventEmitter from ‘events’;

export class Pulser extends EventEmitter {

start() {

setInterval(() => {

console.log(‘${new Date().toISOString()} >>>> pulse’);

this.emit(‘pulse’);

console.log(‘${new Date().toISOString()} <<<< pulse’);

}, 1000);

}

}

This is an ES6 module that defines a class named Pulser. The class inherits from EventEmitter and provides a few methods of its own.

Another thing to examine is how this.emit in the callback function refers to the Pulser object instance. This implementation relies on the ES2015 arrow function. Before arrow functions, our callbacks used a regular function, and this would not have referred to the Pulser object instance. Instead, this would have referred to some other object related to the setInterval function. One of the attributes of arrow functions is that this inside the arrow function has the same value as this in the surrounding context. This means, in this case, that this does refer to the Pulser object instance.

Back when we had to use function, rather than an arrow function, we had to assign this to another variable, as follows:

class Pulser extends EventEmitter {

start() {

var self = this;

setInterval(function() {

self.emit(…);

});

}

}

What’s different is the assignment of this to self. The value of this inside the function is different—it is related to the setInterval function—but the value of self remains the same in every enclosed scope. You’ll see this trick used widely, so remember this in case you come across this pattern in code that you’re maintaining.

If you want to use a simple EventEmitter object but with your own class name, the body of the extended class can be empty:

class HeartBeat extends EventEmitter {}

const beatMaker = new HeartBeat();

The purpose of the Pulser class is to send a timed event once a second to any listeners. The start method uses setInterval to kick off a repeated callback execution that is scheduled for every second and calls emit to send the pulse events to any listeners.

Now, let’s see how we can use the Pulser object. Create a new file called pulsed.mjs, containing the following code:

import { Pulser } from ‘./pulser.mjs’;

// Instantiate a Pulser object const pulser = new Pulser();

// Handler function pulser.on(‘pulse’, () => {

console.log(‘${new Date().toISOString()} pulse received’);

});

// Start it pulsing

pulser.start();

Here, we create a Pulser object and consume its pulse events. Calling pulser.on(‘pulse’) sets up an event listener for the pulse events to invoke the callback function. It then calls the start method to get the process going.

When it is run, you should see the following output:

$ node pulsed.mjs

2020-01-06T06:12:29.530Z >>>> pulse

2020-01-06T06:12:29.534Z pulse received

2020-01-06T06:12:29.534Z <<<< pulse

2020-01-06T06:12:30.538Z >>>> pulse

2020-01-06T06:12:30.539Z pulse received

2020-01-06T06:12:30.539Z <<<< pulse

For each pulse event received, a pulse received message is printed.

That gives you a little practical knowledge of the EventEmitter class. Let’s now look at its operational theory.

3. The EventEmitter theory

With the EventEmitter class, your code emits events that other code can receive. This is a way of connecting two separated sections of your program, kind of like how quantum entanglement means two electrons can communicate with each other from any distance. It seems simple enough.

The event name can be anything that makes sense to you, and you can define as many event names as you like. Event names are defined simply by calling .emit with the event name. There’s nothing formal to do and no registry of event names is required. Simply making a call to .emit is enough to define an event name.

An object sends events using the .emit function. Events are sent to any listeners that have registered to receive events from the object. The program registers to receive an event by calling that object’s .on method, giving the event name and an event handler function.

There is no central distribution point for all events. Instead, each instance of an EventEmitter object manages its own set of listeners and distributes its events to those listeners.

Often, it is required to send data along with an event. To do so, simply add the data as arguments to the .emit call, as follows:

this.emit(‘eventName’, data1, data2, ..);

When the program receives the event, the data appears as arguments to the callback function. Your program listens to this event, as follows:

emitter.on(‘eventName’, (data1, data2, …theArgs) => {

// act on event

});

There is no handshaking between event receivers and the event sender. That is, the event sender simply goes on with its business and it gets no notifications about any events that were received, any action taken, or any errors that occurred.

In this example, we used another one of the ES2015 features—the rest operator—used here in the form of …theArgs. The rest operator catches any number of remaining function parameters into an array. Since EventEmitter can pass along any number of parameters and the rest operator can automatically receive any number of parameters, it’s a match made in heaven—or at least in the TC-39 committee.

We’ve now learned how to use JavaScript classes and how to use the EventEmitter class. What’s next is examining how the HTTPServer object uses EventEmitter.

Source: Herron David (2020), Node.js Web Development: Server-side web development made easy with Node 14 using practical examples, Packt Publishing.

Leave a Reply

Your email address will not be published. Required fields are marked *