What They (probably) Didn’t Teach You, Pt. 1: Node.js Event Emitters & Observer Pattern
Are you listening?
This is an ongoing series where I discuss parts of programming that may have been overlooked in your training/coding classes but could potentially be useful. As such, it assumes that you, the reader, have a working knowledge of JavaScript and related technologies.
What are Event Emitters?
Quite literally, they emit events.
The official about page, linked above, describes Node.js as “an asynchronous event driven JavaScript runtime.”
As such, it’s possible to surmise that events are at the heart of Node’s behavior. But what does this actually mean? To elaborate further, we need a better understanding of what events are and why they are useful.
Let’s talk about events.
Among the very first thing anyone starting in web development is learning how to make webpages respond to user actions: when a certain action occurs, it invokes a function, a.k.a an event listener, which receives the action—translated into objects called events—as the argument.
Running the following script after the DOM generates will make a specified element respond to click events:
document.getElementById('id').addEventListener('click', myListener);function myListener(e) {
e.preventDefault(); // do stuff as side effect
...
}
So what exactly is going on?
The Observer Pattern
The ability to respond to interactions immediately is extremely valuable; without it, user experience will suffer — imagine the frustration, for example, if Microsoft Word didn’t render key presses instantly. But how do we write programs that are constantly waiting to respond to certain input as soon as it is registered?
The simple answer would be to write an infinite loop where each iteration of the loop responds to certain actions. In fact, this is very much how JavaScript engines work given the implementation of the internal event loop. What we need is a lazy method of connecting actions to the event loop: the responders should only execute when there is an input, as opposed to eagerly executing the same code at every moment.
The internal architecture of JavaScript engines can be complicated; understanding it will likely require a highly functional knowledge of OS related concepts such as threading and blocking/non-blocking logic. We’re going to focus on the higher level patterns instead of digging deeper into this rabbit hole.
The observer pattern—so common that it was discussed in the ‘Gang of Four’ book—is precisely meant to address this problem. The pattern uses a system of subscribers subscribing to an observable subject. Any time the observable subject has an update, the subscribers receive the the information and handle the information.
The ‘Gang of Four’ book, often truncated to GoF, refers to Design Patterns: Elements of Reusable Object-Oriented Software(1994) by Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides—hence, the ‘Gang of Four’.
In the context of browsers, DOM objects serve as the subject of user interaction. In our example code above, Document.getElementById('id')
returns a reference to a DOM object with id="id"
, to which we subscribe a handler method using the addEventListener()
method. Now, any time the DOM object is clicked by a user, the object emits an Event
object.
There are multiple classes of Event
objects which are extended varieties of the basic Event
interface. This means most Event
objects contain a number of universal properties and methods (e.g. type
, target
and preventDefault()
) along with properties that are unique to the subtype class (e.g. clicks produce a MouseEvent
, which contains position information such as clientX
and clientY
and keyboard information such as altKey
or ctrlKey
).
This pattern of Event
objects being passed to event listeners is a great example of the observer pattern in implementation. It is important to note that the actual implementation of subjects/event emission is not done by the code we implement, but rather internally by the browser itself. This means that the DOM API exposes subjects to which listeners can be attached without directly revealing the steps where input/output gets translated to the emitted event.
But what about Node.js?
Given that Node’s context is vastly different from the browser all while sharing the same JavaScript engine (Chrome V8 as default, with the option to use Edge’s ChakraCore or Firefox’s SpiderMonkey), Node uses the same concurrency model that gives us the event loop but without the same event ecosystem. This means that while the same subject/observer pattern can be used, the event implementation is inevitably going to be different.
Hence, the EventEmitter
.
How does the Event Emitter work?
Let’s look at the following code blocks:
n.b. Above code will most certainly need to be transpiled down to JavaScript versions that Node can handle.
customEE
, imported from my-module
, is an instance of EventEmitter
which is imported from the Node events
module. It has a interval loop of 3 seconds and a counter: every 3 seconds, it emits a ping
event with the current count attached. On our entry-point file, two listeners are attached to customEE: once
attaches a listener that is invoked once and removed. Running this file on node will produce the following on the console:
$ node main.js
started pinging!
ping #0 from module
ping #1 from module
ping #2 from module
...
n.b.
once
andon
returns the EventEmitter instance, which allows for chaining multiple listener attachments.This file can be downloaded from the following GitHub repo:
Is this actually useful?
Perhaps not; it is relatively unlikely that one would have to use the Event Emitter interface directly.
However, this pattern is replicated in numerous modules and packages that are built for Node: these modules generally produces classes that inherit methods from the EventEmitter
class, which means the methods available to subscribe such as on
or once
are available for these classes as well. Most often, the Event
emitted by these modules are specific to the implementation of the observable subject.
One such example is the popular Socket.io library. Upon examining the source code, one can find the following:
/**
* Module dependencies.
*/
var EventEmitter = require('events').EventEmitter;
var util = require('util');.../**
* Inherits from EventEmitter.
*/
util.inherits(Socket, EventEmitter);
This shows the on
and emit
methods that are available to Socket instances are inherited methods of the Node Event Emitter.