Перейти к содержимому

EventEmitter

EventEmitter — архетипный паттерн pub/sub, встречается на каждом третьем собеседовании. Кроме базового on/off/emit интервьюер проверяет: поддержку once(), возврат функции-отписки, порядок вызова обработчиков при emit и безопасность мутаций Set во время итерации.

Полная реализация EventEmitter: on/once/off/emit, возврат unsubscribe, приватные поля:

class EventEmitter {
#listeners = new Map(); // Map<event, Set<handler>>
/**
* Подписаться на событие.
* @param {string} event
* @param {Function} handler
* @returns {() => void} функция отписки
*/
on(event, handler) {
if (!this.#listeners.has(event)) {
this.#listeners.set(event, new Set());
}
this.#listeners.get(event).add(handler);
return () => this.off(event, handler);
}
/**
* Подписаться один раз: handler вызывается и сразу отписывается.
* @returns {() => void} функция отписки
*/
once(event, handler) {
const wrapper = (...args) => {
this.off(event, wrapper);
handler.apply(this, args);
};
wrapper._original = handler; // для корректного off() по оригинальному handler
return this.on(event, wrapper);
}
/** Отписаться. Ищет как прямое совпадение, так и once-wrapper._original. */
off(event, handler) {
const set = this.#listeners.get(event);
if (!set) return this;
for (const fn of set) {
if (fn === handler || fn._original === handler) {
set.delete(fn);
break;
}
}
if (set.size === 0) this.#listeners.delete(event);
return this;
}
/**
* Вызвать всех подписчиков.
* Копируем Set перед итерацией — защита от мутаций внутри handler-ов.
* @returns {boolean} true если были подписчики
*/
emit(event, ...args) {
const set = this.#listeners.get(event);
if (!set) return false;
for (const fn of [...set]) fn.apply(this, args); // snapshot
return true;
}
removeAllListeners(event) {
event !== undefined
? this.#listeners.delete(event)
: this.#listeners.clear();
return this;
}
listenerCount(event) {
return this.#listeners.get(event)?.size ?? 0;
}
}

Тесты: once, unsubscribe, мутация Set в emit, listenerCount:

const emitter = new EventEmitter();
// ─── on / emit ────────────────────────────────────────────────────────────
const log = [];
emitter.on('data', x => log.push(x));
emitter.emit('data', 1); // log = [1]
emitter.emit('data', 2); // log = [1, 2]
// ─── once срабатывает только один раз ────────────────────────────────────
let count = 0;
emitter.once('connect', () => count++);
emitter.emit('connect'); // count = 1
emitter.emit('connect'); // count = 1 (подписчик уже удалён)
console.assert(count === 1);
// ─── unsubscribe через возвращаемую функцию ───────────────────────────────
const unsub = emitter.on('msg', m => log.push(m));
emitter.emit('msg', 'hello'); // log = [..., 'hello']
unsub();
emitter.emit('msg', 'world'); // игнорируется
console.assert(log.at(-1) === 'hello');
// ─── off() по оригинальному handler (once wrapper) ────────────────────────
const handler = () => log.push('once');
emitter.once('tick', handler);
emitter.off('tick', handler); // удаляет wrapper по _original
emitter.emit('tick'); // handler не вызывается
console.assert(log.at(-1) === 'hello'); // без изменений
// ─── Мутация Set во время emit безопасна ─────────────────────────────────
const order = [];
const u = emitter.on('run', () => { order.push(1); u(); }); // самоотписка
emitter.on('run', () => order.push(2));
emitter.emit('run');
console.assert(order.join() === '1,2'); // оба обработчика вызваны
// ─── listenerCount ────────────────────────────────────────────────────────
console.assert(emitter.listenerCount('run') === 1); // первый отписался
console.assert(emitter.listenerCount('ghost') === 0);

Сложность

  • Время: on/off/once — O(1) (Set); emit — O(k) где k = кол-во подписчиков
  • Память: O(e·k) где e = кол-во уникальных событий, k = среднее кол-во подписчиков

Итог: EventEmitter: Map<event, Set<handler>>, on/once/off/emit, возвращает функцию-отписку, snapshot Set перед emit, корректный off() для once-wrapper через _original.