FossilsðŸ’ŧ CodingImplement Event Emitter
ðŸĶ–DinosaurJavaScriptPatternsArchitecture

Implement Event Emitter

The pub/sub pattern is everywhere — Node.js, DOM events, React, state management. Implementing it tests your understanding of callbacks, memory, and API design.

Implement Event Emitter

Interview Question: "Implement an EventEmitter class with on, off, emit, and once methods."

The Implementation

class EventEmitter {
  #listeners = new Map();
 
  on(event, callback) {
    if (!this.#listeners.has(event)) {
      this.#listeners.set(event, []);
    }
    this.#listeners.get(event).push(callback);
    return this;
  }
 
  off(event, callback) {
    const callbacks = this.#listeners.get(event);
    if (!callbacks) return this;
 
    this.#listeners.set(
      event,
      callbacks.filter(cb => cb !== callback && cb._original !== callback)
    );
    return this;
  }
 
  emit(event, ...args) {
    const callbacks = this.#listeners.get(event);
    if (!callbacks) return false;
 
    callbacks.forEach(cb => cb.apply(this, args));
    return true;
  }
 
  once(event, callback) {
    const wrapper = (...args) => {
      callback.apply(this, args);
      this.off(event, wrapper);
    };
    wrapper._original = callback;
    this.on(event, wrapper);
    return this;
  }
 
  removeAllListeners(event) {
    if (event) {
      this.#listeners.delete(event);
    } else {
      this.#listeners.clear();
    }
    return this;
  }
 
  listenerCount(event) {
    return this.#listeners.get(event)?.length ?? 0;
  }
}

Key points:

  • once wrapper — _original reference allows off to find and remove once listeners
  • Method chaining — return this enables emitter.on('a', fn1).on('b', fn2)
  • emit returns boolean — Indicates whether any listeners were called
  • Private #listeners — Encapsulation via private class field

Usage

const emitter = new EventEmitter();
 
function onData(data) { console.log('Received:', data); }
 
emitter.on('data', onData);
emitter.once('connect', () => console.log('Connected!'));
 
emitter.emit('connect');  // "Connected!"
emitter.emit('connect');  // (nothing — once handler removed)
emitter.emit('data', { id: 1 }); // "Received: { id: 1 }"
 
emitter.off('data', onData);
emitter.emit('data', { id: 2 }); // (nothing)

Follow-Up: "How would you add a max listeners warning?"

constructor(maxListeners = 10) {
  this.#maxListeners = maxListeners;
}
 
on(event, callback) {
  // ... existing code ...
  if (this.#listeners.get(event).length > this.#maxListeners) {
    console.warn(`Possible memory leak: ${event} has ${this.listenerCount(event)} listeners`);
  }
  return this;
}

This mirrors Node.js's EventEmitter behavior — warning about potential memory leaks.