Mediator для коммуникации компонентов

Во Vue.js архитектура приложения строится вокруг компонентов, каждый из которых изолирован по состоянию и логике. Стандартные способы взаимодействия — props для передачи данных вниз и emit для уведомлений вверх — хорошо работают в иерархии «родитель → потомок». Однако при усложнении приложения появляются следующие сложности:

  • компоненты находятся далеко друг от друга в дереве;
  • требуется обмен событиями между «соседними» или независимыми компонентами;
  • возникает каскадная передача props через промежуточные уровни;
  • логика взаимодействия размазывается по всему приложению.

Для решения этой проблемы используется паттерн Mediator, выступающий посредником между компонентами.


Суть паттерна Mediator

Mediator инкапсулирует логику взаимодействия между объектами, позволяя им не знать друг о друге напрямую. Компоненты не общаются между собой напрямую, а взаимодействуют через общий посредник.

Ключевые характеристики:

  • ослабление связности между компонентами;
  • централизованное управление событиями;
  • упрощение поддержки и расширения приложения;
  • устранение «цепочек» props и emit.

Во Vue роль медиатора может выполнять:

  • экземпляр Vue (event bus);
  • внешний объект событий;
  • composable-функция;
  • специализированное хранилище.

Event Bus как простейший Mediator

Классический способ реализации Mediator во Vue — event bus.

// mediator.js
import { reactive } from 'vue'

export const mediator = {
  events: reactive({}),

  on(event, handler) {
    if (!this.events[event]) {
      this.events[event] = []
    }
    this.events[event].push(handler)
  },

  emit(event, payload) {
    if (!this.events[event]) return
    this.events[event].forEach(handler => handler(payload))
  },

  off(event, handler) {
    if (!this.events[event]) return
    this.events[event] =
      this.events[event].filter(h => h !== handler)
  }
}

Mediator хранит список событий и обработчиков, не зная ничего о компонентах-источниках и подписчиках.


Использование Mediator в компонентах

Отправка события

import { mediator } from '@/mediator'

export default {
  methods: {
    saveUser(user) {
      mediator.emit('user:saved', user)
    }
  }
}

Компонент сообщает о событии, не зная, кто и как его обработает.

Подписка на событие

import { mediator } from '@/mediator'

export default {
  mounted() {
    mediator.on('user:saved', this.handleUserSaved)
  },
  beforeUnmount() {
    mediator.off('user:saved', this.handleUserSaved)
  },
  methods: {
    handleUserSaved(user) {
      console.log('Пользователь сохранён', user)
    }
  }
}

Компонент реагирует на событие, не зная источник.


Преимущества Mediator в архитектуре Vue

Изоляция компонентов Компоненты не зависят друг от друга напрямую, что снижает риск побочных эффектов.

Централизация коммуникации Вся логика взаимодействия проходит через единый слой.

Гибкость масштабирования Добавление новых участников взаимодействия не требует изменений существующих компонентов.

Повышение тестируемости Mediator легко подменяется или мокируется в тестах.


Mediator и Composition API

Во Vue 3 Mediator удобно реализовывать через composable.

// useMediator.js
const listeners = new Map()

export function useMediator() {
  function on(event, handler) {
    if (!listeners.has(event)) {
      listeners.set(event, [])
    }
    listeners.get(event).push(handler)
  }

  function emit(event, payload) {
    if (!listeners.has(event)) return
    listeners.get(event).forEach(h => h(payload))
  }

  function off(event, handler) {
    if (!listeners.has(event)) return
    listeners.set(
      event,
      listeners.get(event).filter(h => h !== handler)
    )
  }

  return { on, emit, off }
}

Использование:

import { useMediator } from '@/useMediator'

const mediator = useMediator()

mediator.emit('modal:open')
mediator.on('modal:open', () => {
  console.log('Открытие модального окна')
})

Composable-медиатор не привязан к компонентному дереву и может использоваться в любом месте приложения.


Отличие Mediator от глобального состояния

Mediator:

  • передаёт события, а не состояние;
  • не хранит бизнес-данные;
  • подходит для сигналов, уведомлений, триггеров.

Глобальное состояние (Vuex, Pinia):

  • хранит данные;
  • управляет жизненным циклом состояния;
  • подходит для shared state.

Mediator не заменяет store, а дополняет его, закрывая задачу событийной коммуникации.


Типичные сценарии применения

  • управление модальными окнами;
  • синхронизация действий между виджетами;
  • оповещение о завершении асинхронных операций;
  • взаимодействие плагинов и UI-слоёв;
  • логирование и аналитика событий.

Ограничения и архитектурные риски

Скрытые зависимости Компоненты перестают явно показывать, откуда приходят события.

Отладка Большое количество событий усложняет трассировку потока выполнения.

Перегрузка mediator-а При отсутствии дисциплины mediator может превратиться в «свалку» событий.

Для предотвращения проблем используется:

  • строгая схема именования событий;
  • документация событий;
  • ограничение области ответственности mediator-а.

Именование событий

Рекомендуется использовать неймспейсы:

user:created
user:updated
modal:open
modal:close
form:validated

Такой подход:

  • предотвращает конфликты;
  • упрощает поиск;
  • делает архитектуру прозрачнее.

Mediator как слой архитектуры

В крупных приложениях Mediator выделяется в отдельный уровень:

UI Components
     ↓
   Mediator
     ↓
Business Logic / Services

Компоненты работают только с mediator-ом, не взаимодействуя напрямую ни друг с другом, ни с сервисами.


Когда Mediator оправдан

  • сложная иерархия компонентов;
  • большое количество кросс-компонентных событий;
  • необходимость гибкого расширения функциональности;
  • требования к слабой связности и поддерживаемости кода.

Mediator — архитектурный инструмент, который при осознанном применении значительно упрощает коммуникацию компонентов во Vue.js и делает кодовую базу устойчивой к росту сложности.