Skip to main content

Що таке eventemitter у Node.js?

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', коли файл оновлюється. Хто використовує - вирішує що робити з цим сигналом: гаряче перезавантаження, повторний парсинг конфігу, сповіщення дашборду.

Коротка відповідь

Для співбесіди
Premium

Коротка відповідь допоможе вам впевнено відповідати на цю тему під час співбесіди.

Дочитали статтю?