Node.js позиционируется как «асинхронная среда выполнения JavaScript, управляемая событиями (event-driven)». Это означает, что значительная часть работы Node.js построена на событиях – специальных сигналах, которые генерируются при наступлении определённых ситуаций. В браузере вы, вероятно, сталкивались с событиями вроде кликов мышью или нажатий клавиш. На серверной стороне (бэкенде) под событиями можно понимать, например, установление нового сетевого соединения, завершение чтения файла, приход нового HTTP-запроса и т.д.
Чтобы эффективно управлять такими ситуациями, Node.js предоставляет встроенный модуль events с ключевым классом EventEmitter. Практически любой объект в Node.js, способный генерировать события, является наследником класса EventEmitter. В этом руководстве мы рассмотрим, что такое EventEmitter, зачем он нужен, и как с ним работать. Вы научитесь регистрировать обработчики событий, вызывать (эмитить) события, передавать данные слушателям, удалять обработчики, правильно обрабатывать ошибки, а также увидите примеры использования событий в реальных модулях (fs, http, stream и др.)
Класс EventEmitter: создание и базовое использование
Класс EventEmitter – это сердце событийной системы Node.js. Он позволяет одному объекту испускать событие (emit event), а другому – прослушивать событие (listen for event) и реагировать на него определённым кодом. Таким образом достигается модель «издатель-подписчик»: EventEmitter распространяет (публикует) события, а подписчики (слушатели) выполняют колбэк-функции при получении этих событий.
Создание EventEmitter
Для начала нужно подключить модуль events и создать экземпляр EventEmitter. Это можно сделать двумя способами: напрямую создать экземпляр или унаследоваться в собственном классе.
// Импорт класса EventEmitter из модуля events
const EventEmitter = require('node:events'); // или require('events')
// Способ 1: напрямую создать экземпляр
const emitter1 = new EventEmitter();
// Способ 2: создать свой класс, расширяющий EventEmitter
class MyEmitter extends EventEmitter {}
const emitter2 = new MyEmitter();
Оба варианта дают объект emitter1/emitter2, который умеет генерировать события и реагировать на них. Обычно используют несколько экземпляров EventEmitter для разных целей, чтобы логически разграничить типы событий в приложении. Например, один эмиттер может отвечать за события пользовательского интерфейса, другой – за сетевые события и т.п.
Регистрация обработчиков с помощью emitter.on()
Обработчик события – это функция (колбэк), которая будет вызвана, когда EventEmitter сгенерирует определённое событие. Чтобы зарегистрировать обработчик, используется метод emitter.on(eventName, listener).
eventName – название события (как строка или символ), которое мы хотим слушать. Чаще всего используют строку в camelCase (например, 'dataReceived'), но можно использовать любое имя.
listener – функция-обработчик, которая должна выполняться при наступлении этого события. Эта функция будет вызвана с теми же аргументами, которые передаются при генерации события.
Пример регистрации обработчика:
const emitter = new EventEmitter();
// Регистрация обработчика события "message"
emitter.on('message', (text) => {
console.log('Получено сообщение:', text);
});
В этом примере мы подписались на событие 'message'. Теперь при возникновении данного события наш колбэк выведет переданный текст в консоль. Можно регистрировать несколько обработчиков на одно и то же событие – все они будут вызваны (по порядку регистрации) когда событие произойдёт.
Обработчик регистрируется с использованием метода on(), которому передается два параметра: имя события и callback-функция, принимающая параметры (данные), передаваемые при emit().
Генерация событий с помощью emitter.emit()
Метод emit(eventName, ...args) запускает (генерирует) событие с заданным именем. При вызове emit можно передать дополнительные аргументы ...args – они пойдут в обработчики как параметры.
Например, сгенерируем событие 'message' и передадим ему строку:
// Генерация (испускание) события "message"
emitter.emit('message', 'Привет, мир!');
Когда интерпретатор дойдёт до этой строчки, объект emitter вызовет все зарегистрированные обработчики на событие 'message' синхронно и в том же порядке, в котором они были добавлены. В нашем случае сработает обработчик, который выведет в консоль строку 'Получено сообщение: Привет, мир!'.
Если на момент вызова emit('message', ...) у эмиттера несколько слушателей 'message', они выполнатся последовательно один за другим (в порядке добавления). Это поведение важно: оно обеспечивает предсказуемую последовательность реакции на событи. Обратите внимание: вызовы обработчиков выполняются синхронно в рамках текущего цикла событий. Если нужен асинхронный переход (например, чтобы не блокировать поток выполнения), обработчик сам может запланировать асинхронную операцию с помощью setImmediate() или process.nextTick().
Важно: Если вы вызовете emit для события, на которое нет ни одного слушателя, ничего страшного не произойдёт – ошибки не будет. Метод просто вернёт false, указав, что обработчиков не было (при наличии хотя бы одного слушателя emit возвращает true). В остальном вызов пройдет мимо, и ваше приложение продолжит работу. Это позволяет генерировать события «на аванс» – к моменту, когда появится слушатель, событие может уже пройти незамеченным. Обычно так не делают, но полезно знать, что пустое событие не вызывает исключений.
Ниже приведён полный пример, объединяющий создание эмиттера, добавление обработчика и генерацию события:
const EventEmitter = require('events');
const myEmitter = new EventEmitter();
// Добавляем обработчик события "event"
myEmitter.on('event', () => {
console.log('Событие произошло!');
});
// Генерируем событие "event"
myEmitter.emit('event');`
Консоль выведет:
Событие произошло!
Передача данных слушателям и контекст this
Одно из преимуществ событий – возможность передавать данные от места, где событие произошло, к обработчикам этого события. Метод emit поддерживает передачу произвольного числа аргументов обработчикам. Достаточно указать их после имени события, и они придут в функцию-слушатель соответствующими параметрами.
Рассмотрим пример: пусть событие 'sum' будет передавать два числа, а обработчик будет выводить их сумму:
const emitter = new EventEmitter();
emitter.on('sum', (a, b) => {
console.log(`Сумма равна ${a + b}`);
});
emitter.emit('sum', 5, 7); // Передаём 5 и 7 как аргументы
// Обработчик выведет: "Сумма равна 12"
Как видите, мы при вызове emit('sum', 5, 7) указали два значения, и они поступили в параметры a и b стрелочной функции-обработчика. Вы можете передавать любое количество аргументов таким образом (даже ни одного, если обработчику не нужна дополнительная информация). Главное – следить, чтобы сигнатура (количество и порядок параметров) совпадала у вызова emit и у функции-слушателя.
Теперь обсудим, что будет в переменной this внутри обработчика. По умолчанию, если обработчик объявлен как обычная функция (не стрелочная), то this внутри неё указывает на сам объект-эмиттер, на котором событие произошло. Это может быть полезно, если обработчику нужна ссылка на эмиттер (хотя обычно можно использовать внешнюю переменную из замыкания). Если же обработчик задан стрелочной функцией () => {...}, то у такой функции нет собственного this – она наследует его из внешнего контекста. В результате внутри стрелочного обработчика this не будет ссылаться на эмиттер (скорее всего будет undefined или глобальный объект, в зависимости от режима).
Пример для иллюстрации:
const emitter = new EventEmitter();
// Обычная функция - this будет указывать на emitter
emitter.on('test', function() {
console.log('Обычная функция, this===emitter:', this === emitter);
});
// Стрелочная функция - this не определён в контексте EventEmitter
emitter.on('test', () => {
console.log('Стрелочная, this===emitter:', this === emitter);
});
emitter.emit('test');
// Вывод:
// Обычная функция, this===emitter: true
// Стрелочная, this===emitter: false
В первом обработчике мы использовали function(), и убедились, что this действительно равен emitter. Во втором – стрелочная, где this либо undefined, либо не относится к эмиттеру. Вывод: если вам нужен доступ к самому эмиттеру внутри обработчика, либо используйте обычную функцию, либо храните объект emitter в лексическом окружении и обращайтесь к нему напрямую.
Одноразовые обработчики с помощью emitter.once()
В некоторых случаях нужно, чтобы обработчик отработал только один раз, при первом возникновении события, и затем автоматически удалился. С этой целью EventEmitter предоставляет метод once(eventName, listener). Он работает похоже на on, за тем исключением, что как только событие случится, обработчик будет вызван и тут же сняться (удалиться) автоматически.
Проще говоря, once гарантирует единоразовое выполнение слушателя. Например, это полезно для событий инициализации, которые должны отработать только при первом запуске или при первом соединении.
Рассмотрим пример, сравнивающий on и once:
const emitter = new EventEmitter();
let count = 0;
// Обработчик, который срабатывает на каждое событие
emitter.on('tick', () => {
console.log(`Tick ${++count} (on)`);
});
// Обработчик, который срабатывает только первый раз
emitter.once('tick', () => {
console.log(`Tick ${count} (once) - первое и единственное выполнение`);
});
// Генерируем событие несколько раз
emitter.emit('tick'); // Выведет оба сообщения (count станет 1)
emitter.emit('tick'); // Выведет только сообщение от .on (count станет 2)
emitter.emit('tick'); // Снова только от .on (count станет 3)
При первом вызове emit('tick') работают оба слушателя: и постоянный (on), и одноразовый (once). Одноразовый выводит Tick 0 (once) - первое и единственное выполнение (обратите внимание, мы вывели старое значение count, которое на момент первого вызова было 0, затем счётчик увеличился). После этого once-слушатель удаляется автоматически. Последующие emit('tick') вызывают только постоянный обработчик, который продолжает увеличивать и выводить count.
Под капотом once эквивалентен тому, чтобы вручную повесить обработчик, внутри него вызвать off для самого себя и выполнить логику. Но вам не нужно самим писать эту обёртку – класс EventEmitter всё сделает. Одноразовый слушатель позволяет не беспокоиться о снятии обработчика и предотвращает накопление лишних слушателей, если событие происходит много раз.
Удаление обработчиков событий
Когда обработчики больше не нужны, их следует удалять, чтобы они не вызывались и не занимали память. Существует несколько способов отписаться от событий:
- emitter.removeListener(eventName, listener) – удаляет конкретный обработчик listener для события eventName. Чтобы он сработал, нужно передать точно ту же функцию, которую вы ранее зарегистрировали. У метода есть сокращённое алиас-имя emitter.off(eventName, listener) – можно использовать любое (они эквивалентны).
- emitter.removeAllListeners([eventName]) – удаляет все обработчики. Если указать конкретное название события, удалятся все слушатели только этого события. Если вызвать без аргументов, снимутся все слушатели со всех событий на данном эмиттере. Будьте осторожны: удалять «чужие» обработчики (повешенные в другом модуле) считается плохой практикой, так как это может нарушить работу кода, который рассчитывал на эти события.
Чтобы продемонстрировать снятие слушателя, рассмотрим пример:
const emitter = new EventEmitter();
function responseHandler(data) {
console.log('Ответ получен:', data);
}
emitter.on('response', responseHandler);
emitter.emit('response', 'Первый ответ'); // обработчик выполнится
emitter.removeListener('response', responseHandler);
// emitter.off('response', responseHandler); – то же самое действие
emitter.emit('response', 'Второй ответ'); // обработчик уже снят, ничего не произойдёт
Сначала мы добавили обработчик responseHandler на событие 'response'. После первого вызова события обработчик отработал. Затем мы удалили обработчик через removeListener. Второй вызов emit('response', ...) уже не вызывает ничего, потому что слушателя больше нет. Обратите внимание, что мы сохранили функцию responseHandler в переменной – это важно, ведь чтобы удалить обработчик, нужно передать именно ту же функцию (а не новую, даже с тем же содержимым). Если обработчик был функцией, определённой прямо внутри on(...) (анонимной), удалить её будет сложнее – у вас не будет прямой ссылки. В таких случаях, если потребуется удаление, стоит вынести функцию в переменную или объявить её отдельно.
Также можно удалять всех слушателей разом. Например:
emitter.on('update', handler1);
emitter.on('update', handler2);
// ... добавили два обработчика на "update"
emitter.removeAllListeners('update'); // сняли всех слушателей события "update"
Теперь событие 'update' не имеет ни одного обработчика. Если вызвать emitter.emit('update') – ничего не произойдёт.
Замечание: Метод removeAllListeners() в Node.js можно вызывать с аргументом-именем события или без аргументов. Передача массива событий не поддерживается официально (если передать массив, он трактуется как одно имя события – например, ['message'] как название). Поэтому для удаления нескольких разных событий следует либо вызвать несколько раз, либо не передавать аргумент, чтобы снять всё.
Специальные события 'newListener' и 'removeListener'
Сам EventEmitter генерирует служебные события при добавлении или удалении слушателей:
- 'newListener' – испускается, когда добавляется любой новый обработчик на любое событие. В качестве аргументов передаются имя события и сама функция-обработчик.
- 'removeListener' – испускается после удаления обработчика. Передаются имя события и функция, которая была удалена.
Эти события редко используются новичками, но о них стоит знать. Например, 'newListener' может пригодиться, чтобы автоматически выполнять какие-то действия при подписке на событие (например, сразу же эмитить событие, если оно уже произошло ранее). Обычно же потребности в этих событиях нет, и их применение – довольно продвинутый сценарий.
Получение списка событий и слушателей
Иногда бывает полезно introspect-нуть EventEmitter – узнать, какие события у него есть и сколько обработчиков повешено на каждое. Для этого в API предусмотрены методы:
- emitter.eventNames() – возвращает массив всех имен событий, для которых зарегистрированы хоть какие-то обработчики. Если эмиттер свежий и «пустой», массив будет пустым.
- emitter.listenerCount(eventName) – возвращает число обработчиков, связанных с событием eventName на данном эмиттере. Полезно для диагностики утечек (например, выяснить, не накапливаются ли лишние слушатели).
- emitter.listeners(eventName) – возвращает массив функций-обработчиков, прикреплённых к событию. Вы можете перебрать их или, например, вызвать по отдельности. Учтите, что если некоторые обработчики были добавлены через once, то в этом списке они могут выглядеть как обёртки (Node внутри оборачивает одноразовые слушатели для удаления). Чтобы получить именно исходные функции, есть похожий метод emitter.rawListeners(eventName) – он отличается тем, что для одноразовых слушателей возвращает оригинальную функцию, а не обёртку. В большинстве случаев достаточно обычного listeners().
Пример:
emitter.on('test', () => console.log('A'));
emitter.on('test', () => console.log('B'));
emitter.on('demo', () => console.log('Demo'));
console.log(emitter.eventNames()); // ['test', 'demo']
console.log(emitter.listenerCount('test')); // 2
console.log(emitter.listeners('test')); // [ [Function], [Function] ]
Мы добавили два обработчика на 'test' и один на 'demo'. Метод eventNames() выдал нам два события. listenerCount('test') вернул 2 – что подтверждает наличие двух слушателей. А listeners('test') вернул массив из двух функций (анонимных), которые мы и добавили. Конечно, вы не увидите их кода при выводе, но можно, например, проверить длину массива или сравнить с вашими ссылками, если функции были именованы.
Также начиная с Node.js 18 появился утилитный метод events.getEventListeners(emitter, eventName), который фактически делает то же, что emitter.listeners() (для EventEmitter). Он особенно полезен, если вы работаете с web API EventTarget, но для Node.js эмиттера разницы почти нет.
Ограничение числа слушателей и предупреждение о утечках
По умолчанию EventEmitter разрешает добавить не более 10 обработчиков на одно событие. Если вы добавите 11-й, Node.js не запретит этого делать, но выведет предупреждение в консоль (stderr) о возможной утечке памяти: "Possible EventEmitter memory leak detected. 11 listeners added...". Это защитный механизм: очень часто большое число слушателей на одном событии указывает на ошибку – например, вы случайно многократно регистрируете один и тот же обработчик, не снимая старые. Предупреждение (MaxListenersExceededWarning) призвано обратить ваше внимание на потенциальную проблему.
Это не жёсткий лимит – 11-й и дальнейшие слушатели будут добавлены и вызваны при событии, если вы их сознательно добавляете. Node лишь сигнализирует вам о ситуации, которая может быть ошибкой.
Иногда, однако, большое число слушателей действительно нужно и это не ошибка. В таком случае вы можете настроить лимит самостоятельно:
Для отдельного эмиттера: вызовите emitter.setMaxListeners(n), где n – новое максимальное число слушателей для любого одного события этого эмиттера. Можно поставить больше 10, или, если необходимо, 0 либо Infinity для снятия ограничения совсем. Например: emitter.setMaxListeners(50) позволит до 50 слушателей без предупреждений.
Глобально для всех новых эмиттеров: измените свойство EventEmitter.defaultMaxListeners. Присвоив ему другое число, вы установите новый дефолт для всех экземпляров (учтите, что это повлияет и на уже созданные эмиттеры тоже). Например: EventEmitter.defaultMaxListeners = 20;. Однако локальная настройка через setMaxListeners имеет приоритет над глобальной.
Через модуль events: начиная с Node 15, доступны функции events.setMaxListeners(n, emitter1, emitter2, ...) и events.getMaxListeners(emitter) как альтернативный способ задать или узнать лимит сразу для нескольких объектов или глобально. Внутри они делают то же самое.
Ниже пример изменения лимита и демонстрации предупреждения:
const emitter = new EventEmitter();
emitter.setMaxListeners(1); // лимит в 1 слушателя на событие
// Добавляем два обработчика нарочно
emitter.on('test', () => console.log('Первый слушатель'));
emitter.on('test', () => console.log('Второй слушатель'));
// Node выдаст предупреждение в stderr о превышении лимита
emitter.emit('test'); // Оба обработчика все равно отработают
В этом коде мы искусственно снизили лимит до 1 и добавили два слушателя. В результате при втором добавлении Node выдал предупреждение (его можно увидеть в консоли). Тем не менее, оба обработчика успешно выполнились при вызове события.
Примечание: Установка глобального defaultMaxListeners влияет на все эмиттеры, включая те, что были созданы раньше изменения. Поэтому использовать её нужно осторожно, особенно в библиотеках. Чаще правильнее настраивать лимит точечно для тех объектов, где это необходимо.
**
Обработка ошибок и событие 'error'**
Обработка ошибок в EventEmitter заслуживает особого внимания, потому что с ней связана особая семантика. По соглашению, если при выполнении асинхронной операции происходит ошибка, объект-эмиттер вместо выброса исключения генерирует событие 'error'. Например, стримы (fs.ReadStream, net.Socket и т.п.) при проблемах (не удалось прочитать файл, соединение оборвалось) вызовут у себя this.emit('error', err). Ваше приложение должно быть готово поймать это событие, иначе произойдёт непоправимое: если 'error' событие эмитится и на него нет ни одного подписчика, Node.js считает это исключительной ситуацией и завершает процесс с ошибкой (выводит стек-трейс и останавливается)! Проще говоря, неотловленное 'error' событие эквивалентно выбросу исключения, не обёрнутого в try/catch.
Вот небольшой пример, иллюстрирующий этот момент:
const emitter = new EventEmitter();
// Закомментируйте следующий обработчик, чтобы увидеть падение процесса
emitter.on('error', (err) => {
console.error('Обработано событие ошибки:', err.message);
});
emitter.emit('error', new Error('Что-то пошло не так'));
console.log('Я выполнюсь только если ошибка обработана.');
Если оставить обработчик on('error') – код выведет в консоль сообщение об ошибке и продолжит работу, дойдя до последнего console.log. Но если закомментировать обработчик и попытаться эмитить 'error', Node мгновенно выбросит ошибку на уровне процесса и наш последний лог не выполнится. В реальности это означает, что сервер может упасть, если вы не предусмотрели обработчик ошибок.
Поэтому: всегда добавляйте emitter.on('error', ...) для тех EventEmitter-ов, где ошибка возможна (а в общем случае – для всех, чтобы быть уверенным). Это лучший практический подход рекомендованный официальной документацией.
Стоит знать, что 'error' – имя события особенное. Его особое поведение (автоматическое исключение при отсутствии обработчиков) заложено в самой реализации EventEmitter. Другие события так себя не ведут. Кроме того, начиная с Node 13 появилась возможность устанавливать мониторинг ошибок через символ events.errorMonitor. Если подписаться на emitter.on(events.errorMonitor, handler), то ваш handler будет вызван до стандартных обработчиков 'error', но он не считается «поймавшим» ошибку – то есть даже при его наличии, если нет обычного on('error'), процесс всё равно упадёт. errorMonitor позволяет, например, логировать или метрики снимать по ошибкам, не вмешиваясь в их обработку.
Новый механизм Node.js – capture rejections – связан с обработкой ошибок, возникающих в промисах внутри обработчиков событий. Если в слушателе события вы используете асинхронную функцию (например, emitter.on('data', async () => {...})) и она выбросит исключение или вернёт отклонённый промис, то по умолчанию это приведёт к необработанному rejection (warning, но не падение). Опционально EventEmitter может ловить такие ошибки и преобразовывать их в события 'error'. Для этого при создании эмиттера нужно указать { captureRejections: true } или включить глобально EventEmitter.captureRejections = true. Тогда, если async-обработчик вернул ошибку, она пойдёт в 'error' событие данного эмиттера. Это продвинутая возможность, ею пользуются редко, но полезно знать, что она есть с Node.js 15+. По сути, она делает ошибки в async-функциях эквивалентными синхронным – для единообразия обработки.
Практические примеры использования EventEmitter
Встроенный класс EventEmitter широко используется в Node.js. Многие модули ядра Node и популярные библиотеки наследуют его, чтобы предоставлять интерфейс событий. Рассмотрим несколько реальных примеров, где события помогают выстроить логику.
События файловой системы (модуль fs)
Модуль fs (файловая система) использует события, например, при работе с потоками чтения/записи. Если вы читаете файл через поток (fs.createReadStream), вы получаете объект, который генерирует события:
- 'open' – файл успешно открыт.
- 'data' – пришёл очередной блок данных из файла.
- 'end' – чтение файла завершилось (все данные прочитаны).
- 'error' – произошла ошибка (например, потерян доступ к файлу).
Вот код, иллюстрирующий чтение файла с обработкой событий:
const fs = require('fs');
const readStream = fs.createReadStream('example.txt', { encoding: 'utf-8' });
readStream.on('open', () => {
console.log('Файл открыт для чтения.');
});
readStream.on('data', (chunk) => {
console.log('Получено порция данных:', chunk);
});
readStream.on('end', () => {
console.log('Чтение файла завершено.');
});
readStream.on('error', (err) => {
console.error('Ошибка чтения файла:', err);
});
В этом примере по мере чтения файла example.txt будет многократно эмититься событие 'data' с фрагментами содержимого файла. Когда файл прочитан до конца, стрим вызовет 'end'. Если в процессе что-то пойдёт не так, мы предусмотрели обработчик 'error' – это убережёт наше приложение от краша и позволит отреагировать (например, сообщить пользователю или залогировать ошибку). Обратите внимание: мы подписались на 'open' просто информативно, обычно можно и без этого, но для понимания он показывает момент начала чтения.
Другой пример – функция fs.watch для слежения за изменениями в файле/папке. Она возвращает объект FSWatcher, который является EventEmitter и генерирует события:
`'change' – когда файл изменился (передаются тип изменения и имя файла).
'error' – если возникла ошибка в процессе наблюдения (например, нет доступа).`
Пример использования fs.watch:
const watcher = fs.watch('myDir', (eventType, filename) => {
console.log(`Событие: ${eventType} в файле ${filename}`);
});
watcher.on('error', (err) => {
console.error('Watcher error:', err);
});
Здесь мы следим за папкой myDir. Каждый раз, когда в ней что-то меняется (файл создан, удалён или отредактирован), колбэк из fs.watch будет вызван с описанием. Мы также подписались на 'error' на случай сбоев. (Замечание: fs.watch также может генерировать 'close' когда наблюдение прекращено, но мы не уходим в такие детали.)
События HTTP-сервера (модуль http)
HTTP-сервер в Node.js – ещё один пример EventEmitter’а. Когда вы создаёте сервер через http.createServer(), под капотом создаётся объект http.Server – наследник EventEmitter. Этот сервер генерирует события:
- 'request' – каждый раз, когда приходит новый HTTP-запрос от клиента. Обработчик получает объекты request и response.
- 'listening' – когда сервер начал слушать (то есть выполнил server.listen()).
- 'close' – когда сервер закрыт.
- 'error' – при ошибках, например, если порт занят или произошло что-то неожиданное с сервером.
Событие 'request' чаще всего не обрабатывается через server.on, потому что обычно проще передать колбэк прямо в createServer(callback) – это эквивалентно подписке на 'request'. Но мы можем явно подписаться:
const http = require('http');
const server = http.createServer(); // пока без обработчика
// Логирование каждого запроса
server.on('request', (req, res) => {
console.log(`HTTP ${req.method} запрос ${req.url}`);
res.end('Ваш запрос получен');
});
// Узнаем, когда сервер запустился
server.on('listening', () => {
console.log('Сервер запущен на порту 3000');
});
// Обработка ошибок сервера
server.on('error', (err) => {
console.error('Ошибка сервера:', err);
});
server.listen(3000);
В этом примере мы вручную обработали 'request', 'listening' и 'error'. Когда придёт HTTP-запрос, мы залогируем метод и URL, и отправим ответ. При запуске (listen) сервер сгенерирует 'listening' – мы оповестим, что всё ок. Если что-то пойдёт не так (например, порт 3000 уже занят), сработает 'error', и мы выведем сообщение об ошибке.
Объекты req и res, которые появляются внутри обработчика 'request', сами по себе тоже EventEmitter’ы! Например, req (входящий запрос) – этоReadable Stream, у него есть события 'data', 'end', 'error' (аналогично тому, как с файлом выше). А res (ответ) – это Writable Stream, у него есть события 'finish' (когда ответ полностью отправлен), 'close' (когда соединение закрыто) и 'error'. Таким образом, вся система ввода-вывода HTTP в Node основана на событиях.
Для примера, если нам надо вручную читать тело запроса, мы могли бы сделать:
server.on('request', (req, res) => {
let body = '';
req.on('data', chunk => { body += chunk; });
req.on('end', () => {
console.log('Получены данные запроса:', body);
res.end('OK');
});
});
Здесь мы подписались на события 'data' и 'end' у входящего запроса, чтобы собрать все части тела. Когда 'end' сработает, мы знаем, что запрос полностью получен.
События потоков (модуль stream)
Модуль stream – базовый для многих вещей в Node (файлы, HTTP, сеть). Потоки (Streams) – это тоже EventEmitter’ы. Общие события:
- 'data' – пришёл следующий кусочек данных (для потоков чтения).
- 'end' – конец потока (для потоков чтения).
- 'finish' – закончена запись (для потоков записи).
- 'error' – ошибка в процессе.
- 'close' – поток закрыт (не всегда эмитится, зависит от типа потока, но часто есть).
Мы фактически рассмотрели эти события в контексте файлов и HTTP. Ещё пример: процесс process.stdin (стандартный поток ввода) – тоже Readable Stream. Можно написать:
process.stdin.on('data', chunk => {
console.log('Ввели:', chunk.toString());
});
И терминал будет выдавать событие 'data' каждый раз, когда вы нажимаете Enter с каким-то вводом.
Пользовательские (кастомные) события
Наконец, вы можете использовать EventEmitter в своих собственных классах и модулях для организации архитектуры. Например, вы пишете игру – можно генерировать события вроде 'playerScored', 'gameOver'. Или в приложении есть различные компоненты, и вы хотите, чтобы один компонент уведомлял другой, не создавая жёсткой связи между ними – события отлично подходят для этого (реализация паттерна «наблюдатель»).
Класс EventEmitter можно расширять (наследовать) и добавлять события, описывающие доменную область вашего приложения. Вот искусственный пример: создадим класс Timer, который каждую секунду испускает событие 'tick' и через 5 секунд испускает 'done':
class Timer extends EventEmitter {
start(durationSeconds) {
let elapsed = 0;
const interval = setInterval(() => {
elapsed++;
this.emit('tick', elapsed);
if (elapsed >= durationSeconds) {
clearInterval(interval);
this.emit('done');
}
}, 1000);
}
}
const timer = new Timer();
timer.on('tick', (sec) => console.log(`Прошла ${sec}-я секунда`));
timer.once('done', () => console.log('Таймер завершён!'));
timer.start(5);
// Вывод:
// Прошла 1-я секунда
// Прошла 2-я секунда
// ...
// Прошла 5-я секунда
// Таймер завершён!
Здесь наш класс Timer – это EventEmitter, он сам управляет, когда эмитить события. В методе start каждую секунду вызывается this.emit('tick', elapsed), а когда счётчик достигает нужного значения, генерируется 'done'. Снаружи мы подписались на 'tick' и 'done' – таким образом отделив логику таймера от того, что произойдёт, когда он тикнет или закончится. Это и есть принцип событийного взаимодействия.
Заключение
EventEmitter – мощный инструмент, лежащий в основе архитектуры Node.js. С его помощью реализуется львиная доля асинхронного поведения: от низкоуровневых стримов до высокоуровневых фреймворков. Понимание принципов работы с событиями поможет вам лучше разбираться в коде Node.js, отлаживать приложения и строить собственные модульные системы взаимодействия. Экспериментируйте с событиями, и вы вскоре почувствуете, насколько естественным становится событийно-ориентированный стиль программирования на Node!
Top comments (0)