Before event-driven programming became popular, the standard way to communicate between different parts of an application was pretty straightforward: a component that wanted to send out a message to another one explicitly invoked a method on that component. But event-driven code is written to react rather than be called.
The Benefits Of Eventing
This approach causes our components to be much more decoupled. Basically, as we continue to write an application we’ll identify events along the way, fire them at the right time and attach one or more event listeners to each one. Extending functionality becomes much easier since we can just add on more listeners to a particular event without tampering with the existing listeners or the part of the application where the event was fired from. What we’re talking about is essentially the Observer pattern.
Source: https://www.dofactory.com/javascript/observer-design-pattern
Designing An Event-Driven Architecture
Identifying events is pretty important since we don’t want to end up having to remove/replace existing events from the system, which might force us to delete/modify any number of listeners that were attached to the event. The general principle I use is to consider firing an event only when a unit of business logic finishes execution.
So say you want to send out a bunch of different emails after a user’s registration. Now, the registration process itself might involve many complicated steps, queries etc. But from a business point of view it is one step. And each of the emails to be sent out are individual steps as well. So it would make sense to fire an event as soon as registration finishes and have multiple listeners attached to it, each of which is responsible for sending out one type of email.
Node’s asynchronous, event-driven architecture has certain kinds of objects called “emitters” that emit named events which cause functions called "listeners" to be invoked. All objects that emit events are instances of the EventEmitter class. Using it we can create our own events.
An Example
Let’s use the built-in events module (which I encourage you to check out in detail) to gain access to EventEmitter
.
// my_emitter.js
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
module.exports = myEmitter;
This is the part of the application where our server receives an HTTP request, saves a new user and emits an event accordingly:
// registration_handler.js
const myEmitter = require('./my_emitter');
// Perform the registration steps
// Pass the new user object as the message passed through by this event.
myEmitter.emit('user-registered', user);
And a separate module where we attach a listener:
// listener.js
const myEmitter = require('./my_emitter');
myEmitter.on('user-registered', (user) => {
// Send an email or whatever.
});
It’s a good practice to separate policy from implementation. In this case policy means which listeners are subscribed to which events and implementation means the listeners themselves.
// subscriptions.js
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
// send_email_on_registration.js
module.exports = (user) => {
// Send a welcome email or whatever.
}
This separation allows for the listener to become re-usable too i.e. it can be attached to other events that send out the same message (a user object). It’s also important to mention that when multiple listeners are attached to a single event, they will be executed synchronously and in the order that they were attached. Hence someOtherListener
will run after sendEmailOnRegistration
finishes execution.
However if you want your listeners to run asynchronously you can simply wrap their implementations with setImmediate
like this:
// send_email_on_registration.js
module.exports = (user) => {
setImmediate(() => {
// Send a welcome email or whatever.
});
}
Keep Your Listeners Clean
Stick to the Single Responsibility Principle when writing listeners: one listener should do one thing only and do it well. Avoid, for instance, writing too many conditionals within a listener that decide what to do depending on the data (message) that was transmitted by the event. It would be much more appropriate to use different events in that case:
// registration_handler.js
const myEmitter = require('./my_emitter');
// Perform the registration steps
// The application should react differently if the new user has been activated instantly.
if (user.activated) {
myEmitter.emit('user-registered:activated', user);
} else {
myEmitter.emit('user-registered', user);
}
// subscriptions.js
const myEmitter = require('./my_emitter');
const sendEmailOnRegistration = require('./send_email_on_registration');
const someOtherListener = require('./some_other_listener');
const doSomethingEntirelyDifferent = require('./do_something_entirely_different');
myEmitter.on('user-registered', sendEmailOnRegistration);
myEmitter.on('user-registered', someOtherListener);
myEmitter.on('user-registered:activated', doSomethingEntirelyDifferent);
Detaching Listeners Explicitly When Necessary
In the previous example our listeners were totally independent functions. But in cases where a listener is associated with an object (it’s a method), it has to be manually detached from the events it had subscribed to. Otherwise, the object will never be garbage-collected since a part of the object (the listener) will continue to be referenced by an external object (the emitter). Thus the possibility of a memory-leak.
For example if we’re building a chat application and we want that the responsibility for showing a notification when a new message arrives in a chat room that a user has connected to should lie within that user object itself, we might do this:
// chat_user.js
class ChatUser {
displayNewMessageNotification(newMessage) {
// Push an alert message or something.
}
// `chatroom` is an instance of EventEmitter.
connectToChatroom(chatroom) {
chatroom.on('message-received', this.displayNewMessageNotification);
}
disconnectFromChatroom(chatroom) {
chatroom.removeListener('message-received', this.displayNewMessageNotification);
}
}
When the user closes his/her tab or loses their internet connection for a while, naturally, we might want to fire a callback on the server-side that notifies the other users that one of them just went offline. At this point of course it doesn’t make any sense for displayNewMessageNotification
to be invoked for the offline user, but it will continue to be called on new messages unless we remove it explicitly. If we don’t, aside from the unnecessary call, the user object will also stay in memory indefinitely. So be sure to call disconnectFromChatroom
in your server-side callback that executes whenever a user goes offline.
Beware
The loose coupling in event-driven architectures can also lead to increased complexity if we’re not careful. It can be difficult to keep track of dependencies in our system i.e. which listeners end up executing on which events. Our application will become especially prone to this problem if we start emitting events from within listeners, possibly triggering chains of unexpected events.
Top comments (11)
I'll flesh it out a bit more by providing some related details.
There are a few differences between the EventTarget in the browser and the EventEmitter from Node.
The EventEmitter class implements on, off and emit to register/unregister/fire events. In the EventTarget of window these are called addEventListener, removeEventListener and dispatchEvent.
The EventEmitter is chainable, the EventTarget in the browser is not.
Although on is chainable, off is not. When you remove an event listener using off You can not add another .off to it. In my opinion this is inconsistent and bad practice but I don't want to debate it.
Using EventEmitter, you simply emit any type of object. In a window, EventTarget.dispatchEvent requires the second argument to be of type Event. This means that in order to emit a new event, in the browser you need to instantiate an Event for it.
If you want to deal with events in an even more sophisticated matter then have a look at eventemitter2 on NPM. It normalizes some inconsistencies and adds a few features such as wildcard type event listeners
But it can be done even better. If this stuff interests you then have a look at my Emitter class which is designed specifically to fill these gaps.
Thanks so much for the valuable input Jochem! If you don't mind, I might include
eventemitter2
and wild card-based subscriptions in this article and on another platform where I published it.By all means! Sharing is caring.
May I ask a question, how do I fire a event?
Hey chenge, firing is just another word for "emitting".
Hi Usama thanks. But sendEmailOnRegistration doesn't work, why?
I see. I must subscribe first.
This was a good read. You took a somewhat smaller concept and fleshed it out.
Nice work!
Thank you!
Using event architecture in our APIs will make them more efficiently? Which are the advantages?
The project is divided handlers and listeners?