> Как реализовать хранение нескольких колбэков для одного события в EventEmitter (JavaScript)
Уровень: senior · Роль: frontend · Категория: Технические вопросы
Компании: Т-Банк
Стек: JavaScript
> Пример ответа
Короткий ответ
В EventEmitter хранение нескольких колбэков для одного события реализуется через хранение массива функций в Map или объекте, где ключ - имя события. При вызове on колбэк добавляется в массив, при emit - все функции из массива вызываются последовательно. Для удаления используется фильтрация массива по ссылке на функцию.
Подробное объяснение
Основная идея - использовать структуру данных, где каждому событию соответствует коллекция обработчиков. В JavaScript для этого удобно применять Map или обычный объект. Ключом выступает строка с именем события, значением - массив функций-колбэков.
При регистрации обработчика через on (или addListener) мы проверяем, существует ли уже массив для данного события. Если нет - создаём новый. Затем добавляем колбэк в конец массива. Это позволяет хранить неограниченное количество обработчиков для одного события.
При эмиссии события (emit) мы получаем массив обработчиков по ключу и вызываем каждый из них, передавая аргументы. Важно делать копию массива перед итерацией, чтобы изменения (добавление/удаление обработчиков во время выполнения) не влияли на текущий цикл вызовов.
Удаление обработчика (off или removeListener) реализуется фильтрацией массива - оставляем только те функции, которые не равны удаляемой. Для поддержки once (однократный вызов) можно обернуть колбэк в функцию-обёртку, которая после вызова автоматически удаляет себя.
На практике
В реальных проектах редко пишут EventEmitter с нуля - используют готовые реализации из Node.js (events модуль) или библиотек типа EventEmitter3. Однако понимание внутреннего устройства критично для отладки и оптимизации.
Ключевые моменты реализации:
- Использование
Mapвместо объекта даёт преимущества: гарантированный порядок ключей, отсутствие прототипного загрязнения, возможность использовать нестроковые ключи - При
emitнужно копировать массив обработчиков ([...handlers]илиhandlers.slice()) для безопасной итерации - Метод
onceможно реализовать через обёртку, которая вызываетoffпосле выполнения - Для предотвращения утечек памяти важно корректно удалять обработчики, особенно в SPA при размонтировании компонентов
Пример кода
JAVASCRIPTclass EventEmitter {constructor() {this._events = new Map();}on(event, callback) {if (!this._events.has(event)) {this._events.set(event, []);}this._events.get(event).push(callback);return this; // для chaining}emit(event, ...args) {const handlers = this._events.get(event);if (!handlers) return false;// Копируем массив для безопасной итерации[...handlers].forEach(handler => {handler.apply(this, args);});return true;}off(event, callback) {const handlers = this._events.get(event);if (!handlers) return this;this._events.set(event,handlers.filter(handler => handler !== callback));return this;}once(event, callback) {const wrapper = (...args) => {callback.apply(this, args);this.off(event, wrapper);};this.on(event, wrapper);return this;}}
Как отвечать на собеседовании
Начни с краткого описания структуры: Map с массивами функций. Затем объясни ключевые методы и их реализацию. Упомяни важность копирования массива при emit и почему это нужно. Покажи понимание trade-off между разными структурами данных (Map vs Object). Если спросят про производительность - отметь, что вставка и удаление O(1), а вызов всех обработчиков O(n) где n - количество колбэков.
Что проверяет интервьюер
- Понимание работы событийной модели в JavaScript
- Умение выбирать подходящие структуры данных (Map, массивы)
- Знание тонкостей: безопасная итерация, управление контекстом, предотвращение утечек памяти
- Способность реализовать
onceи поддержку chaining - Понимание разницы между синхронным и асинхронным вызовом обработчиков
Типичные ошибки
- Не копировать массив обработчиков перед итерацией - это приводит к пропуску или повторному вызову колбэков, если они добавляются/удаляются во время emit
- Использовать объект вместо Map - может вызвать проблемы с ключами типа
constructorили__proto__ - Забывать про возврат
thisдля chaining - ломает популярный паттерн - Не обрабатывать случай, когда для события нет обработчиков - приводит к ошибкам при вызове
forEachнаundefined - Реализовывать
onceбез удаления обёртки - вызывает утечку памяти и неожиданные повторные вызовы
> Похожие задачи по frontend
Какая польза от внутренней функции, используемой снаружи внешней функции в JavaScript
Может ли быть несколько экземпляров лексического окружения замыкания в JavaScript
Как улучшить реализацию EventEmitter с использованием Map и Set в JavaScript
Что происходит при встрече промиса во время выполнения синхронного кода в JavaScript
> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?
Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью