JS Tower
Node.jsМодуль 1: Введение в Node.js

События и EventEmitter

Паттерн Observer в Node.js

Цель урока

В этом уроке ты научишься:

  • Использовать EventEmitter
  • Создавать свои события
  • Понимать паттерн Observer

EventEmitter

Многие объекты в Node.js являются эмиттерами событий.

Базовое использование

const EventEmitter = require('events');

const emitter = new EventEmitter();

// Подписка на событие
emitter.on('message', (data) => {
  console.log('Получено:', data);
});

// Генерация события
emitter.emit('message', 'Привет!');
// Получено: Привет!

Несколько аргументов

emitter.on('user', (name, age) => {
  console.log(`${name}, ${age} лет`);
});

emitter.emit('user', 'Иван', 25);
// Иван, 25 лет

Методы EventEmitter

on / addListener

// Подписка на событие (многократный вызов)
emitter.on('event', callback);
emitter.addListener('event', callback); // то же самое

once

// Подписка на одноразовое событие
emitter.once('connect', () => {
  console.log('Подключено!');
});

emitter.emit('connect'); // Подключено!
emitter.emit('connect'); // ничего

off / removeListener

function handler(data) {
  console.log(data);
}

emitter.on('event', handler);
emitter.off('event', handler);
// или
emitter.removeListener('event', handler);

removeAllListeners

// Удалить все обработчики события
emitter.removeAllListeners('event');

// Удалить все обработчики всех событий
emitter.removeAllListeners();

listeners

// Получить массив обработчиков
const handlers = emitter.listeners('event');
console.log(handlers.length);

listenerCount

// Количество обработчиков
const count = emitter.listenerCount('event');

Наследование от EventEmitter

const EventEmitter = require('events');

class Server extends EventEmitter {
  constructor() {
    super();
    this.connections = 0;
  }
  
  connect() {
    this.connections++;
    this.emit('connection', this.connections);
  }
  
  disconnect() {
    this.connections--;
    this.emit('disconnection', this.connections);
  }
}

const server = new Server();

server.on('connection', (count) => {
  console.log(`Новое подключение. Всего: ${count}`);
});

server.on('disconnection', (count) => {
  console.log(`Отключение. Осталось: ${count}`);
});

server.connect(); // Новое подключение. Всего: 1
server.connect(); // Новое подключение. Всего: 2
server.disconnect(); // Отключение. Осталось: 1

Обработка ошибок

const emitter = new EventEmitter();

// Обязательно обрабатывай событие 'error'
emitter.on('error', (err) => {
  console.error('Ошибка:', err.message);
});

// Без обработчика error — процесс упадёт!
emitter.emit('error', new Error('Что-то пошло не так'));

Важно

Если событие error не обработано, Node.js выбросит исключение и завершит процесс.


Асинхронные события

const EventEmitter = require('events');

class DataFetcher extends EventEmitter {
  async fetch(url) {
    try {
      this.emit('start', url);
      
      // Имитация загрузки
      await new Promise(resolve => setTimeout(resolve, 1000));
      const data = { url, timestamp: Date.now() };
      
      this.emit('data', data);
      this.emit('end');
    } catch (error) {
      this.emit('error', error);
    }
  }
}

const fetcher = new DataFetcher();

fetcher.on('start', (url) => console.log('Загрузка:', url));
fetcher.on('data', (data) => console.log('Данные:', data));
fetcher.on('end', () => console.log('Завершено'));
fetcher.on('error', (err) => console.error('Ошибка:', err));

fetcher.fetch('https://api.example.com');

Практические примеры

Logger

const EventEmitter = require('events');

class Logger extends EventEmitter {
  log(level, message) {
    const entry = {
      level,
      message,
      timestamp: new Date().toISOString()
    };
    this.emit('log', entry);
    this.emit(level, entry);
  }
  
  info(message) { this.log('info', message); }
  warn(message) { this.log('warn', message); }
  error(message) { this.log('error', message); }
}

const logger = new Logger();

logger.on('log', (entry) => {
  console.log(`[${entry.timestamp}] ${entry.level}: ${entry.message}`);
});

logger.on('error', (entry) => {
  // Отправить в систему мониторинга
});

logger.info('Приложение запущено');
logger.error('Ошибка подключения');

Job Queue

const EventEmitter = require('events');

class JobQueue extends EventEmitter {
  constructor() {
    super();
    this.jobs = [];
  }
  
  add(job) {
    this.jobs.push(job);
    this.emit('added', job);
  }
  
  async process() {
    while (this.jobs.length > 0) {
      const job = this.jobs.shift();
      this.emit('processing', job);
      
      try {
        await job.execute();
        this.emit('completed', job);
      } catch (error) {
        this.emit('failed', job, error);
      }
    }
    this.emit('empty');
  }
}

Практика

Задание 1: Таймер с событиями

Задача: Создай класс Timer с событиями tick и done.

Решение:

const EventEmitter = require('events');

class Timer extends EventEmitter {
  constructor(seconds) {
    super();
    this.seconds = seconds;
  }
  
  start() {
    let remaining = this.seconds;
    
    const interval = setInterval(() => {
      this.emit('tick', remaining);
      remaining--;
      
      if (remaining < 0) {
        clearInterval(interval);
        this.emit('done');
      }
    }, 1000);
  }
}

const timer = new Timer(5);
timer.on('tick', (s) => console.log(`Осталось: ${s}`));
timer.on('done', () => console.log('Время вышло!'));
timer.start();

Задание 2: Чат

Задача: Создай простой чат с событиями.

Решение:

const EventEmitter = require('events');

class Chat extends EventEmitter {
  constructor() {
    super();
    this.users = [];
  }
  
  join(user) {
    this.users.push(user);
    this.emit('join', user);
  }
  
  leave(user) {
    this.users = this.users.filter(u => u !== user);
    this.emit('leave', user);
  }
  
  send(user, message) {
    this.emit('message', { user, message, time: new Date() });
  }
}

const chat = new Chat();

chat.on('join', (user) => console.log(`${user} присоединился`));
chat.on('leave', (user) => console.log(`${user} вышел`));
chat.on('message', (msg) => console.log(`[${msg.user}]: ${msg.message}`));

chat.join('Иван');
chat.send('Иван', 'Привет всем!');
chat.leave('Иван');

Проверь себя

  1. Что делает метод once?
  2. Почему важно обрабатывать событие error?
  3. Как получить количество обработчиков события?