Множественная подписка

Поведение событийной системы Slim Select

В Slim Select обработка событий строится вокруг конфигурационного объекта events, который передаётся при инициализации экземпляра. Каждое событие представляет собой отдельное свойство, значением которого является функция-обработчик. Типичный набор включает afterChange, beforeOpen, afterOpen, beforeClose, afterClose, error.

const select = new SlimSelect({
  select: '#example',
  events: {
    afterChange: (newVal) => {
      console.log('Изменение значения:', newVal)
    }
  }
})

Такой подход задаёт важное ограничение: каждый ключ события хранит только одну функцию. При повторном назначении обработчика происходит перезапись предыдущего значения. В результате отсутствует нативная поддержка множественной подписки на одно и то же событие.

Проблема перезаписи обработчиков

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

  • обновлять состояние интерфейса
  • отправлять данные на сервер
  • вести логирование
  • синхронизировать с другим компонентом

Прямое объявление нескольких обработчиков невозможно:

events: {
  afterChange: handlerA,
  afterChange: handlerB // перезапишет handlerA
}

Фактически остаётся только последний назначенный обработчик.

Композиция обработчиков через обёртку

Базовый способ организации множественной подписки заключается в создании агрегирующей функции, которая вызывает набор обработчиков.

const afterChangeHandlers = []

function addAfterChange(handler) {
  afterChangeHandlers.push(handler)
}

function removeAfterChange(handler) {
  const index = afterChangeHandlers.indexOf(handler)
  if (index !== -1) afterChangeHandlers.splice(index, 1)
}

const select = new SlimSelect({
  select: '#example',
  events: {
    afterChange: (value) => {
      afterChangeHandlers.forEach(fn => fn(value))
    }
  }
})

Такой подход превращает одиночный callback Slim Select в мини-реализацию pub-sub механизма. Слой абстракции позволяет добавлять и удалять подписки динамически.

Расширение модели до универсального event-emitter

При увеличении количества событий целесообразно использовать централизованный менеджер подписок.

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

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

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

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

const bus = new EventBus()

Интеграция с Slim Select осуществляется через проксирование событий:

const select = new SlimSelect({
  select: '#example',
  events: {
    afterChange: (value) => bus.emit('afterChange', value),
    beforeOpen: () => bus.emit('beforeOpen'),
    afterClose: () => bus.emit('afterClose')
  }
})

Теперь любая логика подключается независимо:

bus.subscribe('afterChange', (value) => {
  console.log('Лог 1:', value)
})

bus.subscribe('afterChange', (value) => {
  console.log('Лог 2:', value)
})

Контроль порядка выполнения обработчиков

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

Дополнительно вводится возможность приоритизации:

subscribe(event, handler, priority = 0) {
  if (!this.events[event]) this.events[event] = []

  this.events[event].push({ handler, priority })
  this.events[event].sort((a, b) => b.priority - a.priority)
}

Исполнение:

emit(event, payload) {
  if (!this.events[event]) return

  this.events[event].forEach(item => item.handler(payload))
}

Такой механизм полезен при смешении критической логики (валидация, блокировка UI) и второстепенных операций (логирование, аналитика).

Удаление подписок и предотвращение утечек памяти

При динамическом создании интерфейсов Slim Select (например, в модальных окнах или SPA-компонентах) критично управлять отпиской обработчиков.

Проблема возникает при сохранении ссылок на функции, которые больше не используются:

bus.subscribe('afterChange', handler)
// компонент уничтожен, но handler остаётся в памяти

Решение заключается в строгом хранении ссылок:

const handler = (value) => {
  console.log(value)
}

bus.subscribe('afterChange', handler)

// позже
bus.unsubscribe('afterChange', handler)

В более сложных сценариях применяется токен подписки:

subscribe(event, handler) {
  const id = Symbol()
  if (!this.events[event]) this.events[event] = []

  this.events[event].push({ id, handler })
  return id
}

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

Интеграция множественной подписки с жизненным циклом Slim Select

Slim Select предоставляет события жизненного цикла, которые удобно использовать как точки расширения системы подписок:

  • beforeOpen — подготовка интерфейса
  • afterOpen — активация UI-логики
  • afterChange — реакция на изменение значения
  • afterClose — финализация состояния

Пример распределения логики:

bus.subscribe('afterOpen', () => {
  console.log('UI активирован')
})

bus.subscribe('afterChange', (value) => {
  validate(value)
  sync(value)
  track(value)
})

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

Декларативная подписка через фабрику событий

Для унификации можно создавать фабричный слой, связывающий Slim Select и event-bus:

function createSlimSelectWithBus(config) {
  const bus = new EventBus()

  const select = new SlimSelect({
    ...config,
    events: {
      afterChange: (v) => bus.emit('afterChange', v),
      afterOpen: () => bus.emit('afterOpen'),
      afterClose: () => bus.emit('afterClose')
    }
  })

  return {
    select,
    on: bus.subscribe.bind(bus),
    off: bus.unsubscribe.bind(bus),
    emit: bus.emit.bind(bus)
  }
}

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

const ss = createSlimSelectWithBus({
  select: '#example'
})

ss.on('afterChange', (v) => updateUI(v))
ss.on('afterChange', (v) => save(v))

Разделение доменной логики через подписки

Множественная подписка становится инструментом архитектурного разделения ответственности. Один и тот же event Slim Select превращается в точку интеграции нескольких независимых слоёв:

  • слой представления
  • слой данных
  • слой аналитики
  • слой валидации
bus.subscribe('afterChange', updateView)
bus.subscribe('afterChange', persistToStore)
bus.subscribe('afterChange', sendAnalytics)
bus.subscribe('afterChange', validateBusinessRules)

Каждая функция существует изолированно и не зависит от порядка других, если не задана явная приоритизация.

Ограничения и особенности подхода

Использование множественной подписки поверх Slim Select добавляет абстракцию, которая требует контроля:

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

При этом достигается расширение базовой модели событий Slim Select до полноценной событийной шины, позволяющей масштабировать взаимодействие компонентов без модификации исходного экземпляра библиотеки.