Tiny event emitter
on, off, emit. Nothing else.
Implement a minimal event emitter with on, off, and emit methods. This is the pattern behind Node’s EventEmitter, browser EventTarget, and most state management libraries.
The Implementation
class EventEmitter { #listeners = new Map();
on(event, fn) { if (!this.#listeners.has(event)) { this.#listeners.set(event, new Set()); } this.#listeners.get(event).add(fn); return () => this.off(event, fn); }
off(event, fn) { this.#listeners.get(event)?.delete(fn); }
emit(event, ...args) { this.#listeners.get(event)?.forEach((fn) => fn(...args)); }}on returns an unsubscribe function — a common pattern that avoids having to hold a reference to fn just to call off later.
Usage
const emitter = new EventEmitter();
const unsubscribe = emitter.on("message", (text) => { console.log("received:", text);});
emitter.emit("message", "hello"); // received: helloemitter.emit("message", "world"); // received: world
unsubscribe();emitter.emit("message", "gone"); // nothingWhy Set Instead of Array
A Set prevents duplicate listeners automatically. If someone calls on('event', fn) twice with the same function, it’s registered once. With an array you’d need a manual deduplication check.
Once Variant
once(event, fn) { const wrapper = (...args) => { fn(...args); this.off(event, wrapper); }; this.on(event, wrapper);}Wraps the listener in a function that removes itself after the first call.
Why It Matters
The pub/sub pattern decouples the thing that fires events from the things that respond to them. Once you understand EventEmitter, you understand how DOM events, WebSockets, and most reactive libraries work under the hood.