How to code your own event emitter in Node.js: a step-by-step guide
If you‘ve worked with Node.js, you‘ve likely used event emitters – a core pillar of Node‘s asynchronous, event-driven architecture. Many of Node‘s built-in modules inherit from the EventEmitter class, allowing them to emit named events and register listener functions.
Coding your own event emitter is a great way to dive deeper into Node and understand a fundamental pattern used heavily in the ecosystem. In this step-by-step guide, we‘ll build up our own EventEmitter class from scratch, mirroring the API and functionality of Node‘s built-in version. Let‘s get started!
Understanding the EventEmitter pattern
At its core, an event emitter is a way for objects to communicate with each other. Emitter objects can emit named events, causing listener functions registered for those events to be called. It‘s a common publish-subscribe pattern for managing asynchronous logic and decoupling modules.
Here‘s a simple example of how event emitters are used:
const EventEmitter = require(‘events‘);
const myEmitter = new EventEmitter();
myEmitter.on(‘someEvent‘, function() {
console.log(‘got a someEvent emission!‘);
});
myEmitter.emit(‘someEvent‘);
// Prints: got a someEvent emission!
In this snippet, we create a new EventEmitter instance, use the on
method to register a listener function for the event ‘someEvent‘
, and then emit that event, causing the listener to be invoked.
Building our EventEmitter class
Now that we understand the basic usage, let‘s start building out our own implementation. We‘ll begin with a skeleton and fill in the methods one-by-one.
class EventEmitter {
constructor() {
this.listeners = {};
}
on(eventName, fn) {}
emit(eventName, ...args) {}
once(eventName, fn) {}
off(eventName, fn) {}
listenerCount(eventName) {}
rawListeners(eventName) {}
}
Our class will have a listeners
property to store a mapping of event names to arrays of listener functions. We‘ll implement on
to register listeners, emit
to trigger events, once
for one-time listeners, off
to remove listeners, and a couple helper methods.
Storing listeners with the on
method
The core of any event emitter is the ability to register listener functions to named events. Let‘s code the on
method to handle this:
on(eventName, fn) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(fn);
return this;
}
This method first checks if there‘s already an array for the given eventName
– if not, it initializes one. It then pushes the given listener function onto that array. Finally, it returns the emitter instance to allow chaining.
We can now register listeners:
const myEmitter = new EventEmitter();
myEmitter.on(‘someEvent‘, () => {
console.log(‘got a someEvent emission!‘);
});
console.log(myEmitter.listeners);
// Prints: { someEvent: [fn] }
Triggering events with the emit
method
With the ability to register listeners in place, we need a way to actually emit events to trigger those listeners. That‘s the job of emit
:
emit(eventName, ...args) {
if (!this.listeners[eventName]) return false;
const fns = this.listeners[eventName];
fns.forEach(fn => fn(...args));
return true;
}
This method first checks if there are any listeners registered for the given eventName
– if not, it returns false
to indicate that.
If there are listeners, it invokes each one, passing through any arguments provided to emit
. Finally, it returns true
to indicate the event had listeners.
Let‘s test it out:
myEmitter.emit(‘someEvent‘, 42, ‘abc‘);
// Prints: got an someEvent emission 42 abc
One-time listeners with once
Sometimes you only want a listener to be called once and then automatically removed. That‘s where once
comes in handy:
once(eventName, fn) {
const onceWrapper = (...args) => {
fn(...args);
this.off(eventName, onceWrapper);
};
this.on(eventName, onceWrapper);
return this;
}
The once
method is a bit trickier. It wraps the provided listener function in another function onceWrapper
. When invoked, this wrapper calls the original fn
, passing through any arguments, and then unregisters itself as a listener.
By registering this onceWrapper
as the actual listener via on
, we get automatic removal after the first event emission. Let‘s see it in action:
myEmitter.once(‘oneTimer‘, () => console.log(‘just once‘));
myEmitter.emit(‘oneTimer‘);
// Prints: just once
myEmitter.emit(‘oneTimer‘);
// Nothing printed, event has no listeners
Removing listeners with off
To give users full control, we need to allow removing listeners. The off
method takes care of this:
off(eventName, fn) {
if (!this.listeners[eventName]) return this;
this.listeners[eventName] =
this.listeners[eventName].filter(f => f !== fn);
return this;
}
Pretty straightforward – off
filters out the provided listener function from the array of listeners for the given event. It returns the emitter instance for chaining.
Inspecting listeners
Finally, let‘s add a couple convenience methods for inspecting the state of the event emitter.
listenerCount
returns the number of listeners for a given event:
listenerCount(eventName) {
if (!this.listeners[eventName]) return 0;
return this.listeners[eventName].length;
}
And rawListeners
returns a copy of the array of listeners itself:
rawListeners(eventName) {
return this.listeners[eventName] || [];
}
Putting it all together
With all the key methods implemented, here‘s the complete code for our EventEmitter class:
class EventEmitter {
constructor() {
this.listeners = {};
}
on(eventName, fn) {
if (!this.listeners[eventName]) {
this.listeners[eventName] = [];
}
this.listeners[eventName].push(fn);
return this;
}
emit(eventName, ...args) {
if (!this.listeners[eventName]) return false;
const fns = this.listeners[eventName];
fns.forEach(fn => fn(...args));
return true;
}
once(eventName, fn) {
const onceWrapper = (...args) => {
fn(...args);
this.off(eventName, onceWrapper);
};
this.on(eventName, onceWrapper);
return this;
}
off(eventName, fn) {
if (!this.listeners[eventName]) return this;
this.listeners[eventName] =
this.listeners[eventName].filter(f => f !== fn);
return this;
}
listenerCount(eventName) {
if (!this.listeners[eventName]) return 0;
return this.listeners[eventName].length;
}
rawListeners(eventName) {
return this.listeners[eventName] || [];
}
}
And here‘s how we could use our custom event emitter:
const myEmitter = new EventEmitter();
myEmitter.on(‘status‘, code => {
console.log(`got status code ${code}`);
});
myEmitter.once(‘alert‘, msg => {
console.log(`ALERT: ${msg}`);
});
myEmitter.emit(‘status‘, 200);
// Prints: got status code 200
console.log(myEmitter.listenerCount(‘status‘));
// Prints: 1
myEmitter.emit(‘alert‘, ‘Shutting down in 5 seconds!‘);
// Prints: ALERT: Shutting down in 5 seconds!
myEmitter.emit(‘alert‘, ‘Shutting down now!‘);
// Nothing printed, the ‘alert‘ event has no more listeners
console.log(myEmitter.rawListeners(‘status‘));
// Prints: [fn]
Going further
In this guide, we‘ve built a working event emitter that handles core functionality. There are more methods and edge cases we could cover to bring it to full parity with Node‘s implementation, but this lays a solid foundation.
A few additional things to explore:
- Handling errors that may be thrown in listener functions
- Supporting a
removeAllListeners
method to wipe out all events - Returning a copy from
rawListeners
to prevent external modification - Optimizing performance for large numbers of listeners
I encourage you to study Node‘s EventEmitter source code to dive even deeper. The event-driven paradigm is incredibly powerful and mastering it will make you a better Node developer.
Happy coding! Let me know if you have any questions or ideas to improve our EventEmitter further.