Запропонувати правкуПокращити цю статтюДопрацюйте відповідь до «Що таке eventemitter у Node.js?». Ваші зміни проходять модерацію перед публікацією.Потрібне підтвердженняКонтентЩо ви змінюєте🇺🇸EN🇺🇦UAПереглядЗаголовок (UA)Коротка відповідь (UA)**EventEmitter** - це клас Node.js з модуля `events`, який дозволяє об'єктам генерувати іменовані події та реєструвати функції-слухачі для їх обробки. ```js const EventEmitter = require('events'); const emitter = new EventEmitter(); emitter.on('message', (msg) => console.log(`Отримано: ${msg}`)); emitter.emit('message', 'hello'); // → Отримано: hello ``` **Ключове:** `emit` синхронний - всі слухачі виконуються до переходу на наступний рядок коду.Показується над повною відповіддю для швидкого нагадування.Відповідь (UA)Зображення**EventEmitter** - це клас Node.js з модуля `events`, який дозволяє об'єктам генерувати іменовані події та реєструвати функції-слухачі для їх обробки. ## Теорія ### Коротко - EventEmitter схожий на дзвінок у ресторанній кухні: дзвониш з іменем ("order ready"), і тільки ті слухачі, що підписані на цю назву, реагують. - `on()` додає постійного слухача; `once()` - одноразового, який видаляється після першого виклику. - `emit()` синхронний: він викликає всіх слухачів по черзі, перш ніж повернути управління. - Завжди слухай подію `'error'`, інакше Node.js викине виняток і завершить процес. - EventEmitter підходить для pub-sub всередині одного процесу; для розподілених систем - Redis або Kafka. ### Швидкий приклад ```js const EventEmitter = require('events'); const emitter = new EventEmitter(); // Постійний слухач - спрацьовує кожного разу emitter.on('order', (dish) => console.log(`Подаємо ${dish}`)); // Одноразовий слухач - видаляється після першого виклику emitter.once('alert', () => console.log('Евакуація!')); emitter.emit('order', 'pizza'); // → Подаємо pizza emitter.emit('alert'); // → Евакуація! emitter.emit('alert'); // → (нічого - вже відпрацював) ``` `on()` тримає слухача живим через усі виклики. `once()` видаляє себе після першого. ### Як це працює всередині EventEmitter зберігає слухачів у внутрішньому об'єкті `_events` - це Map, де ключ є назвою події, а значення - масивом функцій. Коли викликаєш `emit('order', 'pizza')`, Node бере цей масив і синхронно викликає кожну функцію по черзі, передаючи `'pizza'` як аргумент. Ніякої асинхронної черги тут немає. Стек викликів не повертається, поки всі слухачі не відпрацюють. Тому повільний слухач із блокуючим циклом затримає все що йде після нього. ### Коли використовувати - Логін користувача має тригернути і email, і аналітику з одного місця: EventEmitter. - Потрібен сигнал, який спрацює тільки раз (з'єднання встановлено, файл відкрито): `once()`. - Працюєш зі стримами (stream) Node.js: вони вже розширюють EventEmitter і самі емітять `'data'`, `'end'`, `'error'`. - Потрібні події між різними процесами або серверами: пропускай EventEmitter, бери чергу повідомлень - BullMQ або Redis Pub/Sub. ### Патерн власного класу Найпоширеніший підхід у продакшені - розширити EventEmitter безпосередньо: ```js const EventEmitter = require('events'); class Database extends EventEmitter { connect() { setTimeout(() => { this.emit('connected', { host: 'localhost' }); }, 1000); } query(sql) { setTimeout(() => { this.emit('data', [{ id: 1, name: 'Alice' }]); }, 500); } } const db = new Database(); db.on('connected', ({ host }) => { console.log(`Підключено до ${host}`); db.query('SELECT * FROM users'); }); db.on('data', (rows) => { console.log('Рядки:', rows); }); db.connect(); ``` Клас `Database` зосереджується на логіці даних. Хто його використовує - вирішує, що робити з кожною подією. Саме в цьому і є суть розв'язаності. ### Типові помилки **1. Відсутній слухач `'error'`** ```js const ee = new EventEmitter(); ee.emit('error', new Error('boom')); // Необроблений виняток - процес завершується ``` Node.js обробляє `'error'` особливим чином. Емітуєш його без слухача - процес падає. Завжди додавай: ```js ee.on('error', (err) => console.error('Оброблено:', err.message)); ``` **2. Припущення що `emit` є асинхронним** ```js ee.on('heavy', () => { for (let i = 0; i < 1e8; i++) {} // блокуючий цикл }); ee.emit('heavy'); ee.emit('light'); // чекає, поки 'heavy' не завершиться ``` `emit` синхронний. Якщо потрібно перенести важку роботу, використовуй `process.nextTick()` або worker thread всередині слухача. **3. Видалення неправильного слухача** ```js ee.on('greet', () => console.log('hi')); ee.off('greet', () => console.log('hi')); // Слухач лишається! Різні посилання на функцію. ``` Стрілочні функції кожного разу створюють нове посилання. Зберігай функцію окремо: ```js const handler = () => console.log('hi'); ee.on('greet', handler); ee.off('greet', handler); // Тепер правильно ``` **4. Перевищення ліміту слухачів** Node.js попереджає, коли до однієї події додано більше 10 слухачів - це захист від випадкових витоків у циклах. Якщо тобі дійсно потрібно більше, встанови ліміт явно: ```js emitter.setMaxListeners(20); emitter.getMaxListeners(); // → 20 ``` ### Де зустрічається в реальному коді - Стрими Node.js (`fs.ReadStream`, `net.Socket`) розширюють EventEmitter і емітять `'data'`, `'end'`, `'error'`. - `http.Server` емітить `'request'` на кожне вхідне HTTP-з'єднання. - Socket.io використовує EventEmitter як базу для своєї моделі подій між клієнтом і сервером. - Webpack-компілятор емітить `'done'` і `'invalid'` для hot module replacement. - Сам об'єкт `process` є EventEmitter: `'exit'`, `'uncaughtException'`, `'SIGTERM'`. На практиці слухач `'error'` - це те, що команди найчастіше пропускають у прототипах, і це обов'язково дається взнаки на стейджингу. ### Питання на співбесіді **Q:** Яка різниця між `on` і `once`? **A:** `on` додає постійного слухача, який спрацьовує щоразу. `once` обгортає слухача, видаляє його після першого виклику і запускає оригінальну функцію. Використовуй `once` для підтверджень або одноразових кроків ініціалізації. **Q:** `emit` синхронний чи асинхронний? **A:** Синхронний. Він викликає всіх слухачів перш ніж повернути управління. Якщо викликаєш `emit` всередині `setTimeout`, сам emit все одно синхронний у рамках того колбека. **Q:** Що станеться, якщо слухач кине виняток? **A:** Виняток піде вгору по стеку викликів. Якщо є слухач `'error'` і подія яка кинула - це `'error'`, він перехопить. Інакше виняток є необробленим і може завалити процес. **Q:** Як відстежувати витоки пам'яті в EventEmitter? **A:** Використовуй `emitter.eventNames()` для переліку зареєстрованих подій і `emitter.listenerCount('eventName')` для підрахунку слухачів. Node автоматично виводить попередження, коли одна подія має більше 10 слухачів. **Q:** Реалізуй мінімальний EventEmitter з підтримкою `off`. **A:** Використовуй `Map<string, Set<Function>>`. `on` додає до Set, `off` видаляє з нього (видалення за посиланням за O(1)), а `emit` ітерує `Array.from(set)`, щоб уникнути мутації під час циклу. ## Приклади ### Pub-sub з кількома слухачами ```js const EventEmitter = require('events'); const bus = new EventEmitter(); // Два незалежні слухачі на одну подію bus.on('login', (user) => console.log(`Надіслати email до ${user.email}`)); bus.on('login', (user) => console.log(`Трекінг логіну для ${user.id}`)); bus.emit('login', { id: 42, email: 'alice@example.com' }); // → Надіслати email до alice@example.com // → Трекінг логіну для 42 ``` Обидва слухачі спрацьовують у порядку реєстрації. Жоден не знає про існування іншого. Саме ця розв'язаність і робить EventEmitter корисним. ### Логер запитів Express через EventEmitter ```js const EventEmitter = require('events'); const express = require('express'); const app = express(); const logger = new EventEmitter(); logger.on('request', ({ method, url }) => { console.log(`${method} ${url} о ${new Date().toISOString()}`); }); app.use((req, res, next) => { logger.emit('request', { method: req.method, url: req.url }); next(); }); app.get('/users', (req, res) => res.send('Список користувачів')); app.listen(3000); // GET /users → GET /users о 2024-01-15T10:30:00.000Z ``` Обробник маршруту не знає, що логування існує. Заміняй слухача логера будь-коли, не чіпаючи код маршрутів. ### Розширення EventEmitter для відстеження файлів ```js const EventEmitter = require('events'); const fs = require('fs'); class FileWatcher extends EventEmitter { watch(filePath) { fs.watchFile(filePath, { interval: 500 }, (curr, prev) => { if (curr.mtime > prev.mtime) { this.emit('change', { path: filePath, modified: curr.mtime }); } }); } } const watcher = new FileWatcher(); watcher.on('error', (err) => console.error('Помилка watcher:', err)); watcher.on('change', ({ path, modified }) => { console.log(`${path} змінено о ${modified}`); }); watcher.watch('./config.json'); ``` Клас емітить `'change'`, коли файл оновлюється. Хто використовує - вирішує що робити з цим сигналом: гаряче перезавантаження, повторний парсинг конфігу, сповіщення дашборду.Для рев’юераПримітка для модератора (необов’язково)Бачить лише модератор. Прискорює рев’ю.