Build: Event Emitter
Introduction
An Event Emitter implements publish-subscribe in a few methods: on, off, once, and emit. It is small enough to code in an interview but rich enough to expose API design choices.
A robust emitter handles listener removal, one-time listeners, duplicate listeners, listener mutation during emit, and ergonomic unsubscribe functions.
Why This Matters
Emitters appear in Node.js, UI frameworks, analytics SDKs, sockets, and plugin systems. Interviewers like this problem because the happy path is easy, while edge cases reveal whether you understand arrays, references, closures, and mutation safety.
Theory
Data model
Use a Map from event name to an array of listener functions. Arrays preserve registration order, which most event systems guarantee. on pushes a listener and returns an unsubscribe function. off removes a listener by reference.
Mutation safety
If a listener removes another listener during emit, iterating the original array can skip or duplicate callbacks. The safe approach is to copy the listener array before calling functions. That makes each emit operate on the listener snapshot that existed at the start.
once design
once wraps the original listener in a function that unsubscribes itself before invoking the user callback. Store a _original reference on the wrapper so off(event, original) can remove a once-listener before it fires.
Visual Diagrams
events Map login -> [listenerA, onceWrapper] logout -> [listenerB] emit login copy listeners call listenerA call onceWrapper -> removes itself
Copying listeners before emit makes mutation during callbacks predictable.
Code Examples
Complete EventEmitter
This implementation preserves listener order, supports once, and returns unsubscribe functions.
Playground
Press Run to execute the code and see output here.
Output Prediction
Predict the output #1
1function EventEmitter() {2 this.events = new Map();3}4 5EventEmitter.prototype.on = function (eventName, listener) {6 if (!this.events.has(eventName)) {7 this.events.set(eventName, []);8 }9 10 this.events.get(eventName).push(listener);11};12 13EventEmitter.prototype.off = function (eventName, listener) {14 var listeners = this.events.get(eventName) || [];15 this.events.set(eventName, listeners.filter(function (candidate) {16 return candidate !== listener && candidate._original !== listener;17 }));18};19 20EventEmitter.prototype.once = function (eventName, listener) {21 var emitter = this;22 23 function wrapper() {24 emitter.off(eventName, wrapper);25 listener.apply(null, arguments);26 }27 28 wrapper._original = listener;29 this.on(eventName, wrapper);30};31 32EventEmitter.prototype.emit = function (eventName) {33 var listeners = this.events.get(eventName) || [];34 var args = Array.prototype.slice.call(arguments, 1);35 listeners.slice().forEach(function (listener) {36 listener.apply(null, args);37 });38};39 40var bus = new EventEmitter();41function logUser(name) {42 console.log('user:' + name);43}44bus.on('login', logUser);45bus.once('login', function (name) {46 console.log('once:' + name);47});48bus.emit('login', 'Ada');49bus.emit('login', 'Grace');Coding Exercises
Implement EventEmitter
MediumImplement an EventEmitter with these methods:
on(eventName, listener)registers a listener and returns an unsubscribe function.off(eventName, listener)removes that listener. It should also remove aoncewrapper when given the original function.once(eventName, listener)registers a listener that runs at most once.emit(eventName, ...args)calls listeners in registration order and returns whether any listener ran.listenerCount(eventName)returns the number of active listeners.
Constraints:
- Do not use Node's
eventsmodule. - Mutating listeners during
emitshould not corrupt the current emission.
Interview Questions
1Why copy the listeners array before emitting?
Listeners can call off or once wrappers can remove themselves during emission. Iterating a copied snapshot keeps the current emission deterministic and prevents index-shifting bugs.
2What should `on` return?
Returning an unsubscribe function is ergonomic and avoids forcing callers to keep event names and listener references near cleanup code. It is especially useful in UI lifecycles.
Quiz
1. How can `off(event, originalListener)` remove a listener registered with `once`?
Summary
- An EventEmitter maps event names to listener arrays.
- `on` should return an unsubscribe function.
- `once` is a self-removing wrapper.
- `emit` should iterate a snapshot to avoid mutation bugs.
Cheat Sheet
EventEmitter checklist
- Data:
Map<eventName, listeners[]>. on: validate function, push, return unsubscribe.off: filter by listener or wrapper original.once: wrapper removes itself, then calls original.emit: copy listeners, call in order, return boolean.- Edge case: listener mutation during emit.