EventEmitter: основа событийной модели

EventEmitter — встроенный механизм «издатель-подписчик»: объект испускает именованные события, а подписчики на них реагируют. На нём построены потоки, серверы и сокеты Node.

EventEmitter — базовый класс Node, реализующий паттерн «издатель-подписчик»: через emit(name, ...args) объект сообщает о событии, через on(name, listener) на него подписываются. Подписчики не знают друг о друге и об источнике напрямую.

В прошлых уроках потоки общались событиями data, end, error, finish, drain. Откуда они берутся? Все потоки наследуют EventEmitter — единый в Node механизм событий. Понять его — значит понять, как устроена половина платформы: HTTP-сервер испускает request, сокет — connection, процесс — exit. Везде один и тот же класс.

Этот урок — про то, как работает паттерн издатель-подписчик, чем once отличается от on, почему слушатели «утекают» и почему событие error нужно обрабатывать всегда.

Зачем это на практике

  • Развязка кода: одна часть программы «объявляет» событие, другая на него реагирует, и они не зависят друг от друга напрямую.
  • Реакция на жизненный цикл: сервер поднялся, соединение закрылось, задача завершилась — всё это события.
  • Свои события в доменной логике: «пользователь зарегистрировался» → отправить письмо, записать аналитику, выдать бонус — несколько независимых подписчиков.
  • Основа потоков: понимание EventEmitter напрямую объясняет поведение Readable/Writable.

Модуль events — серверный API Node, поэтому кнопки «Запустить» под кодом нет. Запускайте примеры в локальном Node.

on и emit: подписка и публикация

Два основных метода. on(event, listener) подписывает функцию на событие; emit(event, ...args) вызывает всех подписчиков этого события, передавая им аргументы.

const EventEmitter = require('events');
const bus = new EventEmitter();

// подписчик
bus.on('order', (id, sum) => {
  console.log('Новый заказ', id, 'на сумму', sum);
});

// издатель
bus.emit('order', 42, 1500);
bus.emit('order', 43, 800);

Вывод:

Новый заказ 42 на сумму 1500
Новый заказ 43 на сумму 800

Ключевая идея: тот, кто вызывает emit, не знает, сколько подписчиков и кто они. Можно повесить на 'order' три обработчика — отправку письма, запись в лог, обновление счётчика — и emit вызовет все три по очереди. Это и есть развязка: издатель и подписчики не связаны жёстко.

Вызов синхронный: emit по очереди вызывает слушателей прямо сейчас, в порядке подписки, и возвращает управление только после последнего. Это не «отложенное» событие из браузера — это обычный последовательный вызов функций.

once: сработать один раз

once(event, listener) похож на on, но слушатель срабатывает ровно один раз и автоматически отписывается. Это удобно для одноразовых событий: «соединение установлено», «файл готов».

const bus = new EventEmitter();

bus.once('ready', () => console.log('Готов — сработает один раз'));

bus.emit('ready'); // выведет
bus.emit('ready'); // тишина — слушатель уже отписан

Вывод:

Готов — сработает один раз

Сравнение методов:

МетодЧто делает
on(e, fn)подписать fn на каждое событие e (синоним addListener)
once(e, fn)подписать fn на одно ближайшее событие e, затем отписать
off(e, fn)отписать fn от события e (синоним removeListener)
emit(e, ...a)вызвать всех подписчиков e с аргументами a

Важно: чтобы отписаться через off, нужна ссылка на ту же функцию, что передавали в on. Анонимную стрелочную функцию отписать нельзя — её не за что «схватить». Поэтому слушатели, которые планируете снимать, объявляйте как именованные.

Свой класс на основе EventEmitter

Обычно EventEmitter не используют напрямую, а наследуют, чтобы объект «умел» испускать свои события. Так делают все потоки и серверы.

const EventEmitter = require('events');

class Downloader extends EventEmitter {
  start() {
    this.emit('begin');
    // ...загрузка...
    this.emit('progress', 50);
    this.emit('done', { bytes: 1024 });
  }
}

const d = new Downloader();
d.on('progress', (p) => console.log('Прогресс:', p + '%'));
d.on('done', (info) => console.log('Скачано байт:', info.bytes));
d.start();

Вывод:

Прогресс: 50%
Скачано байт: 1024

Теперь Downloader — полноценный издатель событий. Снаружи на него подписываются как на любой поток. Именно так в Node устроены Readable и http.Server: они расширяют EventEmitter и испускают свои именованные события.

Событие error — особое

Одно событие в Node имеет особый статус — 'error'. Если объект испускает 'error', а подписчиков на него нет, EventEmitter не промолчит: он выбросит исключение, и при отсутствии глобального перехвата процесс аварийно завершится.

const bus = new EventEmitter();

// без этой строки следующий emit уронил бы процесс
bus.on('error', (err) => {
  console.error('Поймали ошибку:', err.message);
});

bus.emit('error', new Error('что-то сломалось'));

Вывод:

Поймали ошибку: что-то сломалось

Это сделано намеренно: незамеченная ошибка опаснее, чем явный обработчик. Отсюда правило из урока про потоки — всегда вешать error на потоки и любые эмиттеры. Необработанное 'error' — одна из самых частых причин внезапного падения Node-сервера.

Как это работает под капотом

Внутри EventEmitter — это, по сути, словарь: имя события → массив функций-слушателей. on добавляет функцию в массив по ключу события, emit берёт массив по ключу и вызывает каждую функцию по порядку, синхронно, передавая ей аргументы. once оборачивает слушателя в обёртку, которая после первого вызова сама дёргает off. off ищет функцию в массиве по ссылке и удаляет.

Из «словаря массивов» вытекает важное ограничение: если на одно событие подписать очень много слушателей, массив растёт. Node по умолчанию предупреждает, когда на одно событие повешено больше 10 слушателей: в консоли появляется MaxListenersExceededWarning — почти всегда это признак утечки (где-то on в цикле без парного off). Лимит при необходимости меняют через emitter.setMaxListeners(n), но сначала стоит убедиться, что слушатели не накапливаются по ошибке.

Частые ошибки

  • Нет обработчика error. Эмиттер с событием 'error' без слушателя роняет процесс. Вешайте error на потоки и эмиттеры всегда.
  • Утечка слушателей. on в цикле или на каждый запрос без парного off накапливает функции; предупреждение про >10 слушателей — сигнал утечки.
  • Пытаются off анонимную функцию. Снять можно только слушателя по той же ссылке; анонимную стрелку, переданную в on, отписать нечем.
  • Ждут асинхронности от emit. emit вызывает слушателей синхронно, по порядку, прямо сейчас — это не очередь и не setImmediate.
  • once там, где нужен on (и наоборот). once сработает единожды и отпишется; для повторяющихся событий нужен on.

Итоги

  • EventEmitter реализует «издатель-подписчик»: emit публикует событие, on подписывает слушателей; они развязаны.
  • on — на каждое событие, once — на одно (потом отписка), off — снять слушателя по той же ссылке.
  • emit вызывает слушателей синхронно и по порядку подписки; это обычный последовательный вызов функций.
  • Свои классы наследуют EventEmitter, чтобы испускать собственные события — так устроены потоки и серверы.
  • Событие 'error' без обработчика роняет процесс; >10 слушателей на событие — предупреждение об утечке (правьте парным off).
Проверьте себя
1. Что произойдёт, если объект испустит событие 'error', а подписчиков на 'error' нет?
AEventEmitter выбросит исключение, и при отсутствии глобального перехвата процесс аварийно завершится
BОшибка молча проигнорируется, программа продолжит работу
CСобытие 'error' автоматически превратится в 'warning'
DNode поставит ошибку в очередь и вызовет её позже
2. Чем once(event, fn) отличается от on(event, fn)?
Aonce вызывает слушателя ровно один раз и затем автоматически отписывает его, а on реагирует на каждое событие
Bonce срабатывает асинхронно, а on — синхронно
Conce можно подписать только на событие 'ready'
DРазницы нет, это синонимы
3. Почему предупреждение MaxListenersExceededWarning (больше 10 слушателей на событие) обычно стоит воспринимать как сигнал проблемы?
AЧаще всего это утечка: где-то on вызывается повторно (в цикле или на каждый запрос) без парного off, и слушатели накапливаются
BПотому что больше 10 слушателей Node физически не поддерживает
CПотому что emit при этом начинает работать асинхронно
DПотому что once перестаёт отписывать слушателей