Event Bus и паттерн Observer в JavaScript (Node.js): реализация и примеры

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Event Bus и паттерн Observer в JavaScript (Node.js)

В современной разработке на Node.js часто возникает необходимость организовать слабосвязанное взаимодействие между компонентами приложения. Для решения этой задачи идеально подходят паттерн Observer (Наблюдатель) и его практическая реализация — Event Bus (Шина событий). В этой статье мы подробно разберём, что такое Event Bus, как он связан с паттерном Observer, и создадим собственную реализацию на чистом JavaScript для Node.js.

Что такое паттерн Observer?

Паттерн Observer определяет отношение "один ко многим" между объектами. Когда один объект (субъект или издатель) изменяет своё состояние, все зависимые от него объекты (наблюдатели или подписчики) автоматически уведомляются об этом. В Node.js этот паттерн встроен в ядро через класс EventEmitter.

Основные компоненты паттерна:

  • Subject (Субъект) — объект, который генерирует события.
  • Observer (Наблюдатель) — объект, который подписывается на события.
  • Subscription (Подписка) — механизм регистрации наблюдателя у субъекта.

Что такое Event Bus?

Event Bus — это централизованный посредник, реализующий паттерн Observer. Вместо того чтобы каждый компонент напрямую подписывался на другой компонент, все общаются через единую шину событий. Это даёт следующие преимущества:

  • Слабая связанность компонентов.
  • Простота добавления новых подписчиков без изменения кода издателя.
  • Возможность централизованного управления событиями (логирование, фильтрация, отладка).

Встроенный EventEmitter в Node.js

Node.js предоставляет встроенный модуль events, который содержит класс EventEmitter. Это готовая реализация Event Bus. Рассмотрим базовый пример:

const EventEmitter = require('events');
const eventBus = new EventEmitter();

// Подписываемся на событие 'user:login'
eventBus.on('user:login', (user) => {
    console.log(`Пользователь ${user.name} вошёл в систему`);
});

// Подписываемся ещё одним обработчиком
eventBus.on('user:login', (user) => {
    console.log(`Отправляем приветственное письмо для ${user.email}`);
});

// Генерируем событие
eventBus.emit('user:login', { name: 'Иван', email: 'ivan@example.com' });
// Вывод:
// Пользователь Иван вошёл в систему
// Отправляем приветственное письмо для ivan@example.com

Создание собственного Event Bus

Иногда встроенного EventEmitter недостаточно — например, если нужно добавить поддержку асинхронных обработчиков, приоритетов или строгую типизацию. Давайте создадим собственную реализацию Event Bus с нуля:

class EventBus {
    constructor() {
        this.events = {};
    }

    // Подписка на событие
    on(eventName, callback) {
        if (!this.events[eventName]) {
            this.events[eventName] = [];
        }
        this.events[eventName].push(callback);
        
        // Возвращаем функцию для отписки
        return () => {
            this.events[eventName] = this.events[eventName]
                .filter(cb => cb !== callback);
        };
    }

    // Подписка на одноразовое событие
    once(eventName, callback) {
        const wrapper = (...args) => {
            callback(...args);
            this.off(eventName, wrapper);
        };
        this.on(eventName, wrapper);
    }

    // Генерация события
    emit(eventName, ...args) {
        const callbacks = this.events[eventName];
        if (!callbacks) return false;
        
        callbacks.forEach(callback => {
            try {
                callback(...args);
            } catch (error) {
                console.error(`Ошибка в обработчике события ${eventName}:`, error);
            }
        });
        return true;
    }

    // Отписка от события
    off(eventName, callback) {
        if (!this.events[eventName]) return;
        this.events[eventName] = this.events[eventName]
            .filter(cb => cb !== callback);
    }

    // Удаление всех подписчиков
    removeAllListeners(eventName) {
        if (eventName) {
            delete this.events[eventName];
        } else {
            this.events = {};
        }
    }
}

// Пример использования
const bus = new EventBus();

const unsubscribe = bus.on('data:received', (data) => {
    console.log('Получены данные:', data);
});

bus.emit('data:received', { id: 1, value: 'test' });
// Вывод: Получены данные: { id: 1, value: 'test' }

// Отписываемся
unsubscribe();
bus.emit('data:received', { id: 2 }); // Ничего не произойдёт

Продвинутая реализация с асинхронной поддержкой

В реальных приложениях обработчики часто бывают асинхронными. Добавим поддержку async/await и возможность ожидания завершения всех обработчиков:

                    

Рекомендаций