Event emitters в Node.js

Что такое EventEmitter
EventEmitter — это класс из встроенного модуля events, который позволяет компонентам приложения отправлять уведомления (события) и подписчикам реагировать на них. Краткое определение: объект-издатель уведомляет всех подписчиков о случившемся действии.
Определение термина: Event — именованное уведомление; Listener — функция, выполняемая при наступлении события.
Важно: EventEmitter вызывает слушатели синхронно в порядке регистрации, если не использовать специальные методы как prependListener.
Быстрый пример и инициализация
Перед использованием нужно импортировать класс и создать экземпляр:
const EventEmitter = require("events");
// Создаём экземпляр EventEmitter
const myEmitter = new EventEmitter();Чтобы отправить событие, вызовите метод emit с именем события и необязательными аргументами:
myEmitter.emit("TestEvent", "foo", "bar", 1, 2);При вызове emit все слушатели для этого события вызовутся синхронно в порядке регистрации. Метод вернёт true, если у события были слушатели, и false, если слушателей не было.
Прослушивание событий
Для подписки используйте метод on, который принимает имя события и callback-функцию. Метод возвращает ссылку на EventEmitter, поэтому можно делать цепочки.
// Первый слушатель
myEmitter.on("TestEvent", () => {
console.log("TestEvent Emitted!!!");
});
// Второй слушатель — принимает аргументы
myEmitter.on("TestEvent", (...args) => {
args = args.join(", ");
console.log(`Event emitted with the following arguments: ${args}`);
});
myEmitter.emit("TestEvent", "foo", "bar", 1, 2);Вывод в консоль будет в том порядке, в котором были зарегистрированы слушатели (сначала первый, затем второй).
Изменение порядка выполнения слушателей
Если нужно, чтобы слушатель выполнялся раньше других независимо от порядка регистрации, используйте prependListener:
myEmitter.on("TestEvent", () => {
console.log("TestEvent Emitted!!!");
});
myEmitter.prependListener("TestEvent", () => {
console.log("Executes first")
});
myEmitter.emit("TestEvent", "foo", "bar", 1, 2);Здесь сначала выполнится “Executes first”, затем остальные слушатели. Если регистрировать несколько prependListener, они выполняются в порядке от последнего зарегистрированного к первому (LIFO среди prepend).
Подписка только один раз
Если слушатель должен сработать только при первой эмиссии, используйте once. Он автоматически удалит слушатель после первого вызова.
myEmitter.once("SingleEvent", () => {
console.log("Event handled once");
});
myEmitter.emit("SingleEvent"); // Event handled once
myEmitter.emit("SingleEvent"); // ИгнорируетсяЕсли нужен приоритет для одноразового слушателя, применяйте prependOnceListener.
Обработка ошибок
Особое событие — error. Если EventEmitter эмитирует событие “error” и нет ни одного слушателя для него, Node.js завершит процесс с ошибкой. Поэтому важно явно обрабатывать ошибки через слушателя error.
myEmitter.on("error", (err) => {
console.error(`Error: ${err.message || err}`);
});
myEmitter.emit("error", new Error("This is an error"));Рекомендация: в коде, где возможны исключения внутри слушателей, валидируйте входные данные и обрабатывайте исключения локально. Не полагайтесь на внешние механизмы перехвата ошибок, если хотите избежать аварийного завершения процесса.
Управление слушателями
EventEmitter предоставляет методы для получения и управления слушателями. Их нужно вызывать на экземпляре EventEmitter.
| Метод | Аргументы | Что возвращает / делает |
|---|---|---|
| listenerCount | eventName | Число слушателей для события |
| listeners | eventName | Массив функций-слушателей |
| removeListener | eventName, listener | Удаляет конкретный слушатель (нужно передать ту же функцию) |
| removeAllListeners | eventName? | Удаляет все слушатели для события или для всего эмиттера, если имя не указано |
| setMaxListeners | number | Устанавливает максимум слушателей на событие (по умолчанию 10). 0 или Infinity — безлимит |
Примеры:
function onTest() {
console.log('listener');
}
myEmitter.on('TestEvent', onTest);
console.log(myEmitter.listenerCount('TestEvent')); // количество слушателей
myEmitter.removeListener('TestEvent', onTest); // удаляет указанную функцию
myEmitter.removeAllListeners('TestEvent'); // удаляет всех слушателей TestEvent
myEmitter.setMaxListeners(20); // увеличиваем лимит слушателейОбратите внимание: removeListener требует точно ту же ссылку на функцию, которая была передана в on/once.
Практические шаблоны использования
- Локальные события внутри модуля: использовать EventEmitter, чтобы отделить логику и реакции на события.
- Координация асинхронных шагов: эметируйте события на завершение операций, чтобы запускать следующую логику.
- Инструменты и тесты: эмитируйте события в тестах для имитации внешних сигналов.
Пример: объединение результатов нескольких асинхронных операций через события
const results = [];
myEmitter.on('part', (data) => {
results.push(data);
if (results.length === 3) {
myEmitter.emit('complete', results);
}
});Когда EventEmitter не подходит
- Нужна отказоустойчивая очередь сообщений между процессами — используйте брокер сообщений (RabbitMQ, Kafka).
- Нужна реактивная обработка с композициями потоков и трансформациями — рассмотрите RxJS/Observables.
- Для одноразовых асинхронных операций с ожидаемым результатом удобнее промисы/async-await.
Ментальная модель и эвристики
- Ментальная модель: publish–subscribe/observer. Компонент публикует событие — остальные подписчики получают уведомление.
- Эвристики:
- Подписчики должны быть зарегистрированы до того, как событие может быть эмитировано, если требуется среагировать.
- Используйте once для одноразовых подписок, чтобы избежать утечек памяти.
- Контролируйте количество слушателей через setMaxListeners, чтобы находить потенциальные утечки.
Безопасность и надёжность
- Валидируйте входные данные событий — не доверяйте внешним строкам/объектам без проверки.
- Не исполняйте динамически сформированный код внутри слушателей.
- Ограничивайте влияние ошибок: ловите исключения внутри слушателей и по возможности эмитируйте события error с описательными сообщениями.
- Для критичных систем используйте внешние механизмы очередей и журналирование ошибок.
Диагностика и отладка утечек слушателей
- Ошибка “(node:…) MaxListenersExceededWarning” означает, что количество слушателей превысило значение по умолчанию (10).
- Используйте listenerCount/listeners для поиска места, где накапливаются подписки.
- В тестах после кейса вызывайте removeAllListeners или сохраняйте ссылку на созданные слушатели и удаляйте их.
Факт-бокс
- EventEmitter вызывает слушатели синхронно в порядке регистрации.
- По умолчанию максимум слушателей на событие — 10; можно изменить через setMaxListeners.
- once автоматически удаляет слушатель после первого вызова.
- Если событие “error” эмитируется без слушателей, процесс завершится с ошибкой.
Чек-лист для ролей
Разработчик:
- Зарегистрировал слушатели до возможной эмиссии событий.
- Обработал событие error.
- Использовал once для одноразовых слушателей.
- Удалил слушатели после завершения работы (если нужно).
Code reviewer:
- Проверил, что removeListener получает ту же функцию, что и on.
- Проверил на предмет возможных утечек слушателей.
- Убедился, что данные события валидируются.
Ops/DevOps:
- Настроил мониторинг предупреждений о превышении MaxListeners.
- Логирует критичные события error для последующего анализа.
Мини-методология внедрения событий в модуль
- Определите события интерфейса модуля (пример: ready, data, error, close).
- Документируйте формат данных каждого события и обязательность полей.
- Реализуйте EventEmitter в модуле и экспортируйте экземпляр или класс.
- Покройте тестами сценарии: нормальный поток, многократные эмиссии, отсутствие слушателей, error.
- Внедрите проверку в code review: лимит слушателей, удаление, обработка error.
Примеры тест-кейсов и критерии приёмки
Критерии приёмки:
- Событие вызывается при ожидаемом действии и слушатели получают корректные данные.
- Одноразовый слушатель выполняется ровно один раз.
- При эмиссии error приложение не падает, если есть слушатель error.
- Нет утечек слушателей при многократном использовании модуля.
Тест-кейсы:
- Зарегистрировать два слушателя, вызвать событие, проверить порядок и аргументы.
- Зарегистрировать once, вызвать дважды, убедиться, что слушатель выполнился один раз.
- Эмитировать error без слушателя и с слушателем, проверить поведение процесса в тестовом окружении (мок/спец-настройка).
- Создать много подписчиков и ожидать предупреждения о MaxListeners, затем изменить лимит и проверить отсутствие предупреждения.
Сравнение с альтернативами
- EventEmitter: локальные синхронные события, простой API, не рассчитан на межпроцессное взаимодействие.
- Streams: для потоковых данных (I/O), поддерживают backpressure.
- Promises/async-await: удобны для одиночного результата асинхронной операции.
- Message brokers: надёжная доставка между сервисами, устойчивость к сбоям.
Риск-матрица и смягчение
- Утечка слушателей — риск средней серьезности. Смягчение: setMaxListeners, removeAllListeners, четкая ответственность за удаление.
- Аварийное завершение при error — высокий риск для непрерывных сервисов. Смягчение: всегда обрабатывать event “error” и логировать.
- Неправильный порядок выполнения — низкий/средний. Смягчение: использовать prependListener при необходимости приоритета.
Советы по миграции и совместимости
- При переносе кода на новую версию Node.js проверьте изменения в поведении событий в changelog Node.js.
- Если архитектура становится распределённой, подумайте о переходе от EventEmitter к брокеру сообщений.
Заключение
EventEmitter — мощный и лёгкий инструмент для организации событийной архитектуры внутри Node.js приложения. Он хорошо подходит для локальной координации действий, но требует дисциплины: обработка ошибок, контроль числа слушателей и удаление ненужных подписок помогут избежать сбоев и утечек памяти.
Внедряйте события с чёткой спецификацией формата данных, покрывайте тестами ключевые сценарии и применяйте вышеприведённые шаблоны для надёжности.