> Как реализовать хранение нескольких колбэков для одного события в 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 при размонтировании компонентов

Пример кода

JAVASCRIPT
class 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 без удаления обёртки - вызывает утечку памяти и неожиданные повторные вызовы

> ГОТОВЫ К СЛЕДУЮЩЕМУ СОБЕСЕДОВАНИЮ?

Запустите тренировочную сессию с ИИ и получите детальную обратную связь, чтобы увереннее проходить реальные интервью