EventEmitter в Node.js — как эмитить и слушать

Event emitters — это объекты в Node.js, которые сигнализируют о наступлении события путём отправки сообщения. Модель событий позволяет отделить генерацию событий от их обработки: один модуль эмитит событие, другой реагирует на него.
Определение в одну строку: EventEmitter — класс из встроенного модуля events, который обеспечивает регистрацию слушателей и синхронную передачу аргументов при эмиссии события.
Основные понятия
- Эмитер (EventEmitter) — объект, который может эмитить события.
- Событие (event) — именованный сигнал, например “data” или “error”.
- Слушатель (listener) — функция, которая вызывается при эмиссии события.
Важно: слушатели должны быть зарегистрированы до того, как соответствующее событие будет эмитировано, иначе они не сработают.
Генерация событий
Для работы с EventEmitter не требуется установка сторонних пакетов — модуль events встроен в Node.js. Небольшой пример создания эмитера и эмисии события:
const EventEmitter = require('events');
// Создаём экземпляр EventEmitter
const myEmitter = new EventEmitter();
// Эмитим событие с аргументами
myEmitter.emit('TestEvent', 'foo', 'bar', 1, 2);Метод emit принимает имя события и произвольное количество аргументов. При вызове он синхронно последовательно вызывает всех зарегистрированных слушателей в порядке регистрации (если не применялись методы изменения порядка).
Прослушивание событий
Стандартный метод регистрации слушателя — on. Он принимает имя события и функцию-обработчик и возвращает тот же экземпляр EventEmitter (что позволяет цепочкам вызовов).
// Первый слушатель
myEmitter.on('TestEvent', () => {
console.log('TestEvent Emitted!!!');
});
// Второй слушатель с аргументами
myEmitter.on('TestEvent', (...args) => {
const text = args.join(', ');
console.log(`Event emitted with the following arguments: ${text}`);
});
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”, потом “TestEvent Emitted!!!”. Если вы зарегистрируете несколько prependListener, они выполнятся в порядке от последнего добавленного к первому (LIFO среди prepend).
Однократное прослушивание
Иногда слушатель должен сработать только один раз. Для этого существует метод once — он автоматически удалит слушатель после первого вызова.
myEmitter.once('SingleEvent', () => {
console.log('Event handled once');
});
myEmitter.emit('SingleEvent'); // Event handled once
myEmitter.emit('SingleEvent'); // проигнорированоАналогично prependOnceListener регистрирует одноразовый слушатель, который будет иметь приоритет и выполнится раньше обычных.
Обработка ошибок
Если никакой слушатель не обработает событие с именем “error”, Node.js по умолчанию выбросит необработанную ошибку и процесс завершится. Поэтому важно регистрировать обработчик для событий с именем “error”, если вы планируете эмитить ошибки.
myEmitter.on('error', (err) => {
console.error(`Error: ${err.message}`);
});
myEmitter.emit('error', new Error('This is an error'));Без слушателя по имени “error” эмит события типа error приведёт к аварийному завершению процесса.
Важно: не используйте обработчик error для сокрытия критических ошибок — логируйте и при необходимости пробрасывайте их или корректно завершайте работу.
Управление слушателями
EventEmitter предоставляет методы управления подписками:
- listenerCount(eventName) — количество слушателей для события;
- listeners(eventName) — массив функций-слушателей;
- removeListener(eventName, listener) — удаляет указанного слушателя;
- removeAllListeners([eventName]) — удаляет всех слушателей (все события или для конкретного eventName);
- setMaxListeners(n) — изменяет лимит слушателей на событие (по умолчанию 10).
Пример удаления слушателя:
function handler() {
console.log('Handler');
}
myEmitter.on('TestEvent', handler);
myEmitter.removeListener('TestEvent', handler);Функция removeListener требует точной ссылки на функцию-слушатель — анонимные функции удалить сложнее, поэтому при намерении потом отписаться предпочтительны именованные функции.
Таблица методов EventEmitter
| Метод | Аргументы | Что делает |
|---|---|---|
| emit | eventName, …args | Эмитит событие и синхронно вызывает слушателей |
| on | eventName, listener | Регистрирует слушателя |
| once | eventName, listener | Регистрирует одноразовый слушатель |
| prependListener | eventName, listener | Добавляет слушателя в начало очереди |
| prependOnceListener | eventName, listener | Одноразовый слушатель с приоритетом |
| removeListener | eventName, listener | Удаляет указанного слушателя |
| removeAllListeners | [eventName] | Удаляет всех слушателей (или для события) |
| listeners | eventName | Возвращает массив слушателей |
| listenerCount | eventName | Возвращает число слушателей |
| setMaxListeners | n | Устанавливает предел слушателей (0 или Infinity — без ограничения) |
Факты: по умолчанию maxListeners = 10. Увеличивайте его только при обоснованной необходимости и контролируемом жизненном цикле слушателей.
Когда модель событий даёт преимущества и когда нет
Когда подходит EventEmitter:
- Локальная синхронизация разных модулей внутри одного процесса;
- Реакция на внутренние состояния, потоки данных, ивенты I/O;
- Публикация/подписка в пределах приложения (не для межпроцессного общения).
Когда лучше искать альтернативы:
- Требуется распределённая система и обмен событиями между процессами/серверами — используйте очередь сообщений (RabbitMQ, Redis Pub/Sub, Kafka);
- Нужно управлять асинхронной последовательностью операций с ожидаемой возвратной информацией — рассмотрите Promise/async/await, async iterators или RxJS;
- Нужна гарантия доставки или повторная доставка — используйте специализированные брокеры сообщений.
Альтернативы и сравнение
Краткая матрица выбора:
| Задача | EventEmitter | Promise/async | Message broker | RxJS |
|---|---|---|---|---|
| Локальные простые события | Отлично | Плохо | Плохо | ОК |
| Асинхронные цепочки с гарантией | Средне | Отлично | Хорошо | Отлично |
| Горизонтальное масштабирование | Плохо | Плохо | Отлично | Средне |
| Управление потоком (backpressure) | Ограничено | Средне | Отлично | Отлично |
Выбор зависит от границ ответственности, требований к отказоустойчивости и масштабируемости.
Типичные проблемы и их решения
Утечки слушателей (listener leaks): если вы постоянно добавляете слушатели и не удаляете их, память растёт. Решения: removeListener/removeAllListeners, setMaxListeners, использовать once для одноразовых обработчиков.
Ошибки без обработчика error: регистрируйте общий обработчик ‘error’ или передавайте ошибки по цепочке вызовов через Promise.
Непредсказуемый порядок выполнения: используйте prependListener/append логически либо управляйте порядком через отдельные механизмы (через очереди).
Пример мониторинга количества слушателей:
const count = myEmitter.listenerCount('TestEvent');
console.log(`Listeners for TestEvent: ${count}`);Безопасность и приватность
- Не эмитите объекты ошибок, содержащие чувствительные данные, без надёжной фильтрации — слушатели и логи могут раскрыть их.
- В обработчиках error логируйте минимально необходимую информацию и используйте централизованные системы логирования.
- Если обработчики выполняют код с внешним вводом, валидируйте аргументы.
Тестирование и критерии приёмки
Критерии приёмки:
- Событие эмитится и все зарегистрированные слушатели вызываются в ожидаемом порядке.
- Одноразовые слушатели вызываются ровно один раз и затем удаляются.
- При эмисии ошибки при отсутствии слушателя процесс не аварийно завершается (если это требование приложения).
- Нет утечек слушателей при стресс-тестах (количество слушателей остаётся в допустимых пределах).
Тест-кейсы (минимум):
- Регистрируем два слушателя, эмитим событие — оба сработали в порядке регистрации.
- Регистрируем prependListener, эмитим — prepend сработал первым.
- Регистрируем once, эмитим несколько раз — слушатель вызван только один раз.
- Эмитим ‘error’ при наличии обработчика — ошибка логируется, процесс не падает.
- Добавляем и удаляем слушатель — listenerCount отражает изменения.
Мини‑методология внедрения EventEmitter в проект
- Определите границы: какие события должны быть локальными, а какие — глобальными.
- Создайте централизованные экземпляры (по модулю или по подсистеме) вместо разбросанных эмитеров.
- Документируйте имена событий и формат аргументов (контракт для слушателей).
- Внедрите мониторинг количества слушателей и предупреждения при росте.
- Добавьте тесты для ключевых сценариев (см. критерии приёмки).
Роли и чек‑листы
Разработчик:
- Использовать именованные функции для слушателей, если планируется отписка.
- Не эмитить “error” без обработчика.
- Использовать once для одноразовых задач.
Архитектор:
- Определить, когда нужен брокер сообщений вместо EventEmitter.
- Задокументировать контракты событий.
Операции (DevOps):
- Настроить алерты на рост числа слушателей и частые ошибки ‘error’.
- Обеспечить сбор логов и трассировки ошибок.
Примеры для реальных сценариев
- Координатор задач в памяти:
const tasks = new EventEmitter();
tasks.on('task:done', (id, result) => {
console.log(`Task ${id} completed: ${result}`);
});
function completeTask(id, result) {
tasks.emit('task:done', id, result);
}- Поток данных — эмитим чанки и финализируем:
const streamEvents = new EventEmitter();
streamEvents.on('data', chunk => processChunk(chunk));
streamEvents.on('end', () => finishProcessing());
streamEvents.emit('data', Buffer.from('...'));
streamEvents.emit('end');Модель принятия решения (Mermaid)
flowchart TD
A[Нужно ли межпроцессное взаимодействие?] -->|Да| B[Использовать брокер сообщений]
A -->|Нет| C[Локальные события]
C --> D{Требуется порядок и гарантия доставки?}
D -->|Да| B
D -->|Нет| E[EventEmitter]Советы по миграции и совместимости
- При переводе логики на распределённую архитектуру замените локальные emit на публикацию в брокер.
- При миграции с ранних версий Node проверяйте доступность методов prependListener и prependOnceListener (они появились достаточно давно, но всегда полезно тестировать)
- В браузерных сборках EventEmitter можно заменить на легковесные реализации (events-полифилы) или использовать встроенные DOM-события для компонентов.
Короткое резюме
EventEmitter — простой и мощный инструмент для синхронной связи модулей внутри процесса Node.js. Он удобен для локальных событий и логики, где не требуется надёжная доставка между машинами. Контролируйте число слушателей, обрабатывайте ‘error’ и выбирайте альтернативы (Promise, брокеры сообщений, RxJS) там, где нужны другие гарантии.
Важно: документируйте контракт событий и тестируйте критические сценарии, чтобы избежать утечек и неожиданных завершений процесса.
Похожие материалы
Как найти приложения для Android Wear 2.0
Как размыть фон в Canva: 3 простых способа
Установить и протестировать Windows 10 S
RRoD на Xbox 360: как устранить и диагностировать
Как проверить версию Ubuntu