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).