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 = 1emitter.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 по _originalemitter.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.