How do you implement a simple pub/sub event emitter?
A pub/sub (publish–subscribe) emitter lets parts of an app communicate through named events, without direct references.
subscribe(event, fn) — register a listener under the event name.
publish(event, data) — call every listener registered for that event with the data.
unsubscribe — remove a listener so it stops receiving events.
Benefit: loose coupling — publishers and subscribers don't need to know about each other.
class EventBus {
#events = {};
subscribe(event, callback) {
(this.#events[event] ??= []).push(callback);
// Return unsubscribe function
return () => {
this.#events[event] = this.#events[event].filter(cb => cb !== callback);
};
}
publish(event, ...data) {
(this.#events[event] ?? []).forEach(cb => cb(...data));
}
subscribeOnce(event, callback) {
const unsub = this.subscribe(event, (...args) => {
callback(...args);
unsub();
});
}
}
// Usage
const bus = new EventBus();
const unsub = bus.subscribe('message', (text, sender) => {
console.log(`${sender}: ${text}`);
});
bus.subscribe('message', (text) => {
console.log(`Log: ${text}`);
});
bus.publish('message', 'Hello!', 'John');
// 'John: Hello!'
// 'Log: Hello!'
unsub(); // Remove first subscriber
bus.publish('message', 'World!', 'Jane');
// 'Log: World!' (only second subscriber)#events stores arrays of callbacks per event. subscribe adds a callback and returns an unsubscribe function (closure). publish calls all callbacks for the event. subscribeOnce auto-unsubscribes after first call. ??= lazily initializes the array. Private #events prevents external access.
Implement subscribe (with unsubscribe return), publish, and once — this is a common interview coding question. The unsubscribe closure is the key detail.
Using private fields (#events) and ??= shows modern JS. This is the foundation for Redux, custom event systems, and component communication.