Пошук уроків, статей та іншого контенту
Створите подієві об’єкти, зареєструєте слухачів і керуватимете їхнім життєвим циклом через EventEmitter.
Node.js активно використовує подієву модель. Замість того щоб постійно перевіряти стан об’єкта, код може підписатися на подію та виконатися лише тоді, коли ця подія відбудеться.
Подія має:
ім’я — рядок, наприклад connected або data;
відправника — об’єкт, який генерує подію;
слухача — функцію, яка виконується у відповідь;
дані — необов’язкові аргументи, передані слухачу.
У Node.js для створення подієвих об’єктів використовується клас EventEmitter із вбудованого модуля node:events.
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('message', (text) => {
console.log(`Отримано повідомлення: ${text}`);
});
emitter.emit('message', 'Привіт, Node.js!');Результат:
Отримано повідомлення: Привіт, Node.js!Метод emit() синхронно запускає всіх слухачів події з указаним іменем.
Зазвичай EventEmitter використовують як базовий клас для власного об’єкта.
const { EventEmitter } = require('node:events');
class TaskRunner extends EventEmitter {
run(taskName) {
this.emit('started', taskName);
try {
const result = `${taskName} виконано`;
this.emit('completed', result);
} catch (error) {
this.emit('failed', error);
}
}
}
const runner = new TaskRunner();
runner.on('started', (taskName) => {
console.log(`Початок завдання: ${taskName}`);
});
runner.on('completed', (result) => {
console.log(`Успішне завершення: ${result}`);
});
runner.on('failed', (error) => {
console.error(`Помилка: ${error.message}`);
});
runner.run('Завантаження даних');У цьому прикладі:
TaskRunner успадковує можливості EventEmitter.
Метод run() генерує події started, completed або failed.
Код, який використовує TaskRunner, лише підписується на ці події.
Сам TaskRunner не знає, що саме робитимуть слухачі.
Такий підхід зменшує зв’язність між компонентами: об’єкт повідомляє про факт, а інші частини програми самостійно вирішують, як реагувати.
on()Метод on() додає слухача, який викликатиметься щоразу під час генерації події.
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('tick', (value) => {
console.log(`Слухач 1: ${value}`);
});
emitter.on('tick', (value) => {
console.log(`Слухач 2: ${value}`);
});
emitter.emit('tick', 1);
emitter.emit('tick', 2);Результат:
Слухач 1: 1
Слухач 2: 1
Слухач 1: 2
Слухач 2: 2Для однієї події можна зареєструвати довільну кількість слухачів. Вони викликаються в порядку реєстрації.
addListener()addListener() — це синонім on():
emitter.addListener('ready', () => {
console.log('Об’єкт готовий');
});У новому коді зазвичай використовують коротший і поширеніший метод on().
once()once() реєструє слухача, який виконається лише один раз. Після першого виклику він автоматично видаляється.
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.once('connected', () => {
console.log('З’єднання встановлено');
});
emitter.emit('connected');
emitter.emit('connected');Результат:
З’єднання встановленоДругий виклик emit() не запускає слухача.
once() корисний для подій на кшталт:
першої ініціалізації;
одноразового підключення;
завершення запуску;
отримання першої відповіді.
Для генерування події використовується emit():
emitter.emit('eventName', argument1, argument2);Назва події може бути будь-яким рядком. Дані передаються слухачам як аргументи.
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('userCreated', (user, createdAt) => {
console.log(`Створено користувача ${user.name}`);
console.log(`Час: ${createdAt.toISOString()}`);
});
emitter.emit(
'userCreated',
{ id: 1, name: 'Олена' },
new Date()
);emit() повертає логічне значення:
true, якщо для події був хоча б один слухач;
false, якщо слухачів не було.
const wasHandled = emitter.emit('unknown');
console.log(wasHandled); // falseЦе може бути корисно, коли потрібно перевірити, чи хтось обробляє певну подію.
Слухачі EventEmitter виконуються синхронно та послідовно в межах виклику emit().
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('step', () => {
console.log('Перший слухач');
});
emitter.on('step', () => {
console.log('Другий слухач');
});
console.log('До emit');
emitter.emit('step');
console.log('Після emit');Результат:
До emit
Перший слухач
Другий слухач
Після emitЯкщо слухач виконує тривалу синхронну операцію, наступні слухачі чекатимуть її завершення. Тому слухачі мають виконувати свою роботу передбачувано й не блокувати подієвий цикл.
Асинхронний код усередині слухача запускається за правилами відповідного API:
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('data', async (value) => {
await Promise.resolve();
console.log(`Асинхронна обробка: ${value}`);
});
console.log('До emit');
emitter.emit('data', 42);
console.log('Після emit');У цьому прикладі виклик emit() не очікує завершення async-слухача. Він лише синхронно викликає функцію, а подальше виконання після await відбувається пізніше.
Подекуди слухач потрібно видалити: наприклад, після завершення роботи компонента або перед повторною реєстрацією обробника.
Для видалення потрібне саме те посилання на функцію, яке було передано під час реєстрації.
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
function handleMessage(message) {
console.log(`Повідомлення: ${message}`);
}
emitter.on('message', handleMessage);
emitter.emit('message', 'Перше повідомлення');
emitter.off('message', handleMessage);
emitter.emit('message', 'Друге повідомлення');Результат:
Повідомлення: Перше повідомленняМетод off() видаляє один зареєстрований екземпляр слухача. Також для цього можна використовувати removeListener():
emitter.removeListener('message', handleMessage);off() є зручнішим сучасним записом для цієї операції.
Такий слухач не має доступного зовнішнього посилання:
emitter.on('message', (message) => {
console.log(message);
});Створити ідентичну функцію пізніше недостатньо:
emitter.off('message', (message) => {
console.log(message);
});Це інший об’єкт-функція, тому початковий слухач не буде видалений. Якщо слухач може знадобитися для видалення, збережіть його в змінній або оголосіть іменовану функцію.
Компонент, який підписується на події, має також уміти відписуватися від них.
const { EventEmitter } = require('node:events');
class Monitor {
constructor(source) {
this.source = source;
this.handleData = this.handleData.bind(this);
this.started = false;
}
start() {
if (this.started) {
return;
}
this.source.on('data', this.handleData);
this.started = true;
}
stop() {
if (!this.started) {
return;
}
this.source.off('data', this.handleData);
this.started = false;
}
handleData(value) {
console.log(`Монітор отримав: ${value}`);
}
}
const source = new EventEmitter();
const monitor = new Monitor(source);
monitor.start();
source.emit('data', 'A');
monitor.stop();
source.emit('data', 'B');Результат:
Монітор отримав: AМетод stop() відписує монітор від джерела. Це важливо для довгоживучих процесів: незакриті підписки можуть утримувати об’єкти в пам’яті та спричиняти повторну обробку подій.
Метод bind() у конструкторі потрібен, щоб під час виклику слухача this посилався на екземпляр Monitor. Крім того, збережене посилання на прив’язану функцію можна використати для off().
EventEmitter надає методи для перевірки зареєстрованих слухачів.
listenerCount()Повертає кількість слухачів для події:
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('data', () => {});
emitter.on('data', () => {});
console.log(emitter.listenerCount('data')); // 2eventNames()Повертає імена подій, для яких є слухачі:
console.log(emitter.eventNames()); // [ 'data' ]listeners()Повертає масив слухачів певної події:
const listeners = emitter.listeners('data');
console.log(listeners.length); // 2Такі методи корисні для діагностики та тестування. Не варто будувати основну логіку програми на постійному перегляді внутрішнього стану слухачів.
errorПодія error має спеціальне значення. Якщо EventEmitter генерує error, але для неї немає слухача, Node.js викидає помилку, і процес може завершитися.
const { EventEmitter } = require('node:events');
const emitter = new EventEmitter();
emitter.on('error', (error) => {
console.error(`Сталася помилка: ${error.message}`);
});
emitter.emit('error', new Error('Не вдалося підключитися'));Якщо подієвий об’єкт може генерувати error, зареєструйте обробник під час його ініціалізації.
class Connection extends EventEmitter {
connect() {
this.emit('error', new Error('Сервер недоступний'));
}
}
const connection = new Connection();
connection.on('error', (error) => {
console.error(error.message);
});
connection.connect();Обробник error має не просто приховувати проблему. Він повинен записати її, повідомити користувача або виконати іншу визначену програмою дію.
Нижче наведено подієвий об’єкт, який імітує завантаження файлу. Він генерує події початку, прогресу, завершення та помилки.
const { EventEmitter } = require('node:events');
class FileUploader extends EventEmitter {
upload(fileName, totalParts) {
if (totalParts <= 0) {
this.emit('error', new Error('Кількість частин має бути більшою за нуль'));
return;
}
this.emit('start', fileName);
let uploadedParts = 0;
const timer = setInterval(() => {
uploadedParts += 1;
this.emit('progress', {
fileName,
uploadedParts,
totalParts,
percent: Math.round((uploadedParts / totalParts) * 100)
});
if (uploadedParts === totalParts) {
clearInterval(timer);
this.emit('complete', fileName);
}
}, 100);
}
}
const uploader = new FileUploader();
uploader.on('start', (fileName) => {
console.log(`Почато завантаження: ${fileName}`);
});
uploader.on('progress', ({ fileName, percent }) => {
console.log(`${fileName}: ${percent}%`);
});
uploader.once('complete', (fileName) => {
console.log(`Завантаження завершено: ${fileName}`);
});
uploader.on('error', (error) => {
console.error(`Помилка: ${error.message}`);
});
uploader.upload('report.pdf', 5);Кожен слухач відповідає лише за свою реакцію:
start повідомляє про початок;
progress відображає прогрес;
complete повідомляє про завершення;
error обробляє помилки.
Подієвий об’єкт не залежить від способу відображення прогресу. Його можна використовувати в CLI-програмі, серверному модулі або тестах.
Якщо метод ініціалізації викликається кілька разів, слухач може бути доданий кілька разів:
function initialize() {
emitter.on('data', handleData);
}
initialize();
initialize();
emitter.emit('data', 'value');handleData виконається двічі. Для захисту:
викликайте ініціалізацію лише один раз;
перевіряйте стан компонента;
відписуйтеся перед повторною підпискою;
використовуйте once(), якщо подія одноразова.
Для off() потрібне те саме посилання на функцію, яке використовувалося в on().
const handleData = (value) => {
console.log(value);
};
emitter.on('data', handleData);
emitter.off('data', handleData);errorПодія error без слухача може призвести до аварійного завершення процесу. Для об’єктів, які можуть генерувати помилки, обробник потрібно зареєструвати заздалегідь.
emit() дочекається асинхронних слухачівemit() не повертає проміс і не очікує завершення async-слухачів. Якщо програмі потрібно координувати асинхронні результати, це слід організувати окремо, а не покладатися на сам факт генерації події.
Якщо подія передає об’єкт, кілька слухачів можуть змінювати його. Краще передавати зрозумілу структуру даних і домовитися, чи має слухач право її змінювати.
EventEmitter є базовим механізмом подій у Node.js.
Подієвий об’єкт можна створити через new EventEmitter() або успадкувати від EventEmitter.
on() реєструє багаторазового слухача.
once() реєструє слухача для одноразового виконання.
emit() генерує подію та передає їй аргументи.
off() і removeListener() видаляють слухачів.
Для видалення потрібне те саме посилання на функцію.
Слухачі виконуються синхронно та в порядку реєстрації.
Подію error потрібно обробляти окремим слухачем.
Компоненти мають керувати життєвим циклом підписок: додавати їх під час запуску та видаляти під час завершення роботи.