Implementing a Custom Event Emitter in JavaScript



Implementing a custom Event Emitter in JavaScript involves creating a system that allows objects to communicate with each other through events. This pattern is widely used in Node.js and other event-driven architectures. In this episode, we'll create a custom Event Emitter that supports registering event listeners, emitting events, and removing listeners.


What is an Event Emitter?

An Event Emitter is a pattern that allows for communication between different parts of an application. It lets you define events, register listeners for those events, and emit events to invoke the registered listeners.

Real Interview Insights

Interviewers might ask you to:

  • Implement a custom Event Emitter.
  • Handle registering and emitting events.
  • Support removing event listeners.
  • Handle edge cases such as emitting events with no listeners.

Implementing Custom Event Emitter

Here’s an implementation of a custom Event Emitter:

class EventEmitter {
  constructor() {
    this.events = {};
  }
 
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    return this;
  }
 
  off(event, listener) {
    if (!this.events[event]) return this;
    this.events[event] = this.events[event].filter(l => l !== listener);
    return this;
  }
 
  once(event, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(event, onceWrapper);
    };
    this.on(event, onceWrapper);
    return this;
  }
 
  emit(event, ...args) {
    if (!this.events[event]) return this;
    this.events[event].forEach(listener => listener.apply(this, args));
    return this;
  }
 
  removeAllListeners(event) {
    if (this.events[event]) {
      delete this.events[event];
    }
    return this;
  }
}
Explanation:
  • Event Storage: Use an object to store event listeners.
  • Registering Events: Add listeners to the event's array.
  • Removing Events: Filter out the specified listener from the event's array.
  • One-time Listeners: Wrap the listener to ensure it only executes once.
  • Emitting Events: Invoke all listeners for the specified event.
  • Remove All Listeners: Delete all listeners for a specified event.

Practical Examples

Consider examples of using the custom Event Emitter:

const emitter = new EventEmitter();
 
const listener1 = (msg) => console.log(`Listener 1: ${msg}`);
const listener2 = (msg) => console.log(`Listener 2: ${msg}`);
 
emitter.on('event1', listener1);
emitter.on('event1', listener2);
 
emitter.emit('event1', 'Hello World');
// Output:
// Listener 1: Hello World
// Listener 2: Hello World
 
emitter.off('event1', listener2);
 
emitter.emit('event1', 'Hello Again');
// Output:
// Listener 1: Hello Again
 
emitter.once('event2', (msg) => console.log(`Once Listener: ${msg}`));
 
emitter.emit('event2', 'This happens once');
// Output:
// Once Listener: This happens once
 
emitter.emit('event2', 'This will not be logged');
 
emitter.removeAllListeners('event1');
emitter.emit('event1', 'This will not be logged');

Handling Edge Cases

  1. No Listeners: Handle cases where events are emitted with no registered listeners.
  2. Multiple Listeners: Ensure multiple listeners for the same event are handled correctly.
  3. Removing Listeners: Ensure listeners can be removed and do not get called after removal.

Enhanced Implementation with Additional Features

class EventEmitter {
  constructor() {
    this.events = {};
  }
 
  on(event, listener) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(listener);
    return this;
  }
 
  off(event, listener) {
    if (!this.events[event]) return this;
    this.events[event] = this.events[event].filter(l => l !== listener);
    return this;
  }
 
  once(event, listener) {
    const onceWrapper = (...args) => {
      listener.apply(this, args);
      this.off(event, onceWrapper);
    };
    this.on(event, onceWrapper);
    return this;
  }
 
  emit(event, ...args) {
    if (!this.events[event]) return this;
    this.events[event].forEach(listener => listener.apply(this, args));
    return this;
  }
 
  removeAllListeners(event) {
    if (this.events[event]) {
      delete this.events[event];
    }
    return this;
  }
 
  listenerCount(event) {
    return this.events[event] ? this.events[event].length : 0;
  }
 
  rawListeners(event) {
    return this.events[event] ? [...this.events[event]] : [];
  }
}
 
// Example usage with additional features
const emitter = new EventEmitter();
 
const listener3 = (msg) => console.log(`Listener 3: ${msg}`);
emitter.on('event3', listener3);
 
console.log(emitter.listenerCount('event3')); // Output: 1
console.log(emitter.rawListeners('event3')); // Output: [listener3]
 
emitter.emit('event3', 'Test Event');
// Output:
// Listener 3: Test Event
 
emitter.off('event3', listener3);
 
console.log(emitter.listenerCount('event3')); // Output: 0

Use Cases for Custom Event Emitter

  1. Component Communication: Facilitating communication between different parts of an application.
  2. Event-Driven Architecture: Implementing event-driven systems where components react to specific events.
  3. Custom Events: Creating custom events for specific application needs.