EventBus (Pub/Sub)
Минималистичная реализация шины событий (publish/subscribe) без сторонних библиотек — для связи между несвязанными модулями.
Задача
Два модуля приложения должны обмениваться данными, не зная друг о друге. Прямые импорты создают жёсткие зависимости. EventBus позволяет издателю публиковать событие, а подписчику — реагировать на него независимо.
Решение
// event-bus.ts
type EventHandler<T = unknown> = (payload: T) => void;
class EventBus {
private events: Map<string, Set<EventHandler>> = new Map();
on<T>(event: string, handler: EventHandler<T>): => void {
if (!this.events.has(event)) {
this.events.set(event, new Set);
}
this.events.get(event)!.add(handler as EventHandler);
// Возвращает функцию отписки
return => this.off(event, handler);
}
once<T>(event: string, handler: EventHandler<T>): void {
const wrapper: EventHandler<T> = (payload) => {
handler(payload);
this.off(event, wrapper as EventHandler);
};
this.on(event, wrapper);
}
off<T>(event: string, handler: EventHandler<T>): void {
this.events.get(event)?.delete(handler as EventHandler);
}
emit<T>(event: string, payload?: T): void {
this.events.get(event)?.forEach((handler) => handler(payload));
}
clear(event?: string): void {
if (event) {
this.events.delete(event);
} else {
this.events.clear();
}
}
}
// Синглтон для всего приложения
export const bus = new EventBus();
Использование:
import { bus } from './event-bus';
// Модуль A: подписка
const unsubscribe = bus.on<{ userId: number }>('user:login', ({ userId }) => {
console.log('Пользователь вошёл:', userId);
});
// Модуль B: публикация
bus.emit('user:login', { userId: 42 });
// Одноразовая подписка
bus.once('app:ready', () => console.log('Приложение готово'));
// Отписка
unsubscribe;
Ключевые моменты
onвозвращает функцию отписки — удобнее, чем хранить ссылку на handler отдельно.SetвместоArrayисключает дублирование одного и того же обработчика.onceавтоматически удаляет обработчик после первого вызова.- Синглтон через именованный экспорт
bus— один экземпляр на всё приложение.
Варианты
- mitt — 200 байт, TypeScript, популярная минималистичная замена.
- EventTarget — встроенный браузерный API, можно использовать без кастомной реализации:
const bus = new EventTarget(); bus.dispatchEvent(new CustomEvent('user:login', { detail: { userId: 42 } })); bus.addEventListener('user:login', (e) => console.log(e.detail)); - В React — предпочти
Context+useReducerвместо глобального EventBus.
Когда НЕ использовать
- Когда данные нужно хранить, а не просто передавать — используй глобальный стейт (Zustand, Redux).
- Когда события связаны с жизненным циклом компонентов React — предпочти хуки и Context.