Another important and fundamental pattern used in Node.js is the observer pattern. Together with reactor, callbacks, and modules, this is one of the pillars of the platform and an absolute prerequisite for using many node-core and userland modules.
Observer is an ideal solution for modeling the reactive nature of Node.js, and a perfect complement for callbacks. Let's give a formal definition as follows:
Note
Pattern (observer): defines an object (called subject), which can notify a set of observers (or listeners), when a change in its state happens.
The main difference from the callback pattern is that the subject can actually notify multiple observers, while a traditional continuation-passing style callback will usually propagate its result to only one listener, the callback.
In traditional object-oriented programming, the observer pattern requires interfaces, concrete classes, and a hierarchy; in Node.js, all becomes much simpler. The observer pattern is already built into the core and is available through the EventEmitter class. The EventEmitter
class allows us to register one or more functions as listeners, which will be invoked when a particular event type is fired. The following image visually explains the concept:
The EventEmitter
is a prototype, and it is exported from the events
core module. The following code shows how we can obtain a reference to it:
The essential methods of the EventEmitter
are given as follows:
on(event, listener)
: This method allows you to register a new listener (a function) for the given event type (a string)
once(event, listener)
: This method registers a new listener, which is then removed after the event is emitted for the first time
emit(event, [arg1], […])
: This method produces a new event and provides additional arguments to be passed to the listeners
removeListener(event, listener)
: This method removes a listener for the specified event type
All the preceding methods will return the EventEmitter
instance to allow chaining. The listener
function has the signature, function([arg1], […])
, so it simply accepts the arguments provided the moment the event is emitted. Inside the listener, this
refers to the instance of the EventEmitter
that produces the event.
We can already see that there is a big difference between a listener and a traditional Node.js callback; in particular, the first argument is not an error, but it can be any data passed to emit()
at the moment of its invocation.
Create and use an EventEmitter
Let's see how we can use an EventEmitter
in practice. The simplest way is to create a new instance and use it directly. The following code shows a function, which uses an EventEmitter
to notify its subscribers in real time when a particular pattern is found in a list of files:
The EventEmitter
created by the preceding function will produce the following three events:
fileread
: This event occurs when a file is read
found
: This event occurs when a match has been found
error
: This event occurs when an error has occurred during the reading of the file
Let's see now how our findPattern()
function can be used:
In the preceding example, we registered a listener for each of the three event types produced by the EventEmitter
which was created by our findPattern()
function.
The EventEmitter
- as it happens for callbacks - cannot just throw exceptions when an error condition occurs, as they would be lost in the event loop if the event is emitted asynchronously. Instead, the convention is to emit a special event, called error
, and to pass an Error
object as an argument. That's exactly what we are doing in the findPattern()
function that we defined earlier.
Note
It is always a good practice to register a listener for the error
event, as Node.js will treat it in a special way and will automatically throw an exception and exit from the program if no associated listener is found.
Make any object observable
Sometimes, creating a new observable object directly from the EventEmitter
class is not enough, as this makes it impractical to provide functionality that goes beyond the mere production of new events. It is more common, in fact, to have the need to make a generic object observable; this is possible by extending the EventEmitter
class.
To demonstrate this pattern, let's try to implement the functionality of the findPattern()
function in an object as follows:
The FindPattern
prototype that we defined extends the EventEmitter
using the inherits()
function provided by the core module util
. This way, it becomes a full-fledged observable class. The following is an example of its usage:
We can now see how the FindPattern
object has a full set of methods, in addition to being observable by inheriting the functionality of the EventEmitter
.
This is a pretty common pattern in the Node.js ecosystem, for example, the Server
object of the core http
module defines methods such as listen()
, close()
, setTimeout()
, and internally it also inherits from the EventEmitter
function, thus allowing it to produce events, such as request
, when a new request is received, or connection
, when a new connection is established, or closed
, when the server is closed.
Other notable examples of objects extending the EventEmitter
are Node.js streams. We will analyze streams in more detail in Chapter 3, Coding with Streams.
Synchronous and asynchronous events
As with callbacks, events can be emitted synchronously or asynchronously, and it is crucial that we never mix the two approaches in the same EventEmitter
, but even more importantly, when emitting the same event type, to avoid to produce the same problems that we described in the Unleashing Zalgo section.
The main difference between emitting synchronous or asynchronous events lies in the way listeners can be registered. When the events are emitted asynchronously, the user has all the time to register new listeners even after the EventEmitter
is initialized, because the events are guaranteed not to be fired until the next cycle of the event loop. That's exactly what is happening in the findPattern()
function. We defined this function previously and it represents a common approach that is used in most Node.js modules.
On the contrary, emitting events synchronously requires that all the listeners are registered before the EventEmitter
function starts to emit any event. Let's look at an example:
If the ready
event was emitted asynchronously, then the previous code would work perfectly; however, the event is produced synchronously and the listener is registered after the event was already sent, so the result is that the listener is never invoked; the code will print nothing to the console.
Contrarily to callbacks, there are situations where using an EventEmitter
in a synchronous fashion makes sense, given its different purpose. For this reason, it's very important to clearly highlight the behavior of our EventEmitter
in its documentation to avoid confusion, and potentially a wrong usage.
EventEmitter vs Callbacks
A common dilemma when defining an asynchronous API is to check whether to use an EventEmitter
or simply accept a callback. The general differentiating rule is semantic: callbacks should be used when a result must be returned in an asynchronous way; events should instead be used when there is a need to communicate that something has just happened.
But besides this simple principle, a lot of confusion is generated from the fact that the two paradigms are most of the time equivalent and allow you to achieve the same results. Consider the following code for an example:
The two functions helloEvents()
and helloCallback()
can be considered equivalent in terms of functionality; the first communicates the completion of the timeout using an event, the second uses a callback to notify the caller instead, passing the event type as an argument. But what really differentiates them is the readability, the semantic, and the amount of code that is required to be implemented or used. While we cannot give a deterministic set of rules to choose between one or the other style, we can certainly provide some hints to help take the decision.
As a first observation, we can say that callbacks have some limitations when it comes to supporting different types of events. In fact, we can still differentiate between multiple events by passing the type as an argument of the callback, or by accepting several callbacks, one for each supported event. However, this cannot exactly be considered an elegant API. In this situation, an EventEmitter
can give a better interface and leaner code.
Another case where the EventEmitter
might be preferable is when the same event can occur multiple times, or not occur at all. A callback, in fact, is expected to be invoked exactly once, whether the operation is successful or not. The fact that we have a possibly repeating circumstance should let us think again about the semantic nature of the occurrence, which is more similar to an event that has to be communicated rather than a result; in this case an EventEmitter
is the preferred choice.
Lastly, an API using callbacks can notify only that particular callback, while using an EventEmitter
function it's possible for multiple listeners to receive the same notification.
Combine callbacks and EventEmitter
There are also some circumstances where an EventEmitter
can be used in conjunction with a callback. This pattern is extremely useful when we want to implement the principle of small surface area by exporting a traditional asynchronous function as the main functionality, while still providing richer features, and more control by returning an EventEmitter
. One example of this pattern is offered by the
node-glob
module (https://npmjs.org/package/glob), a library that performs glob-style file searches. The main entry point of the module is the function it exports, which has the following signature:
The function takes pattern
as the first argument, a set of options
, and a callback
function which is invoked with the list of all the files matching the provided pattern. At the same time, the function returns an EventEmitter
that provides a more fine-grained report over the state of the process. For example, it is possible to be notified in real-time when a match occurs by listening to the match
event, to obtain the list of all the matched files with the end
event, or to know whether the process was manually aborted by listening to the abort
event. The following code shows how this looks:
As we can see, the practice of exposing a simple, clean, and minimal entry point while still providing more advanced or less important features with secondary means is quite common in Node.js, and combining EventEmitter
with traditional callbacks is one of the ways to achieve that.
Tip
Pattern: create a function that accepts a callback and returns an EventEmitter
, thus providing a simple and clear entry point for the main functionality, while emitting more fine-grained events using the EventEmitter
.