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.

Similar Posts