Синхронизация нескольких экземпляров

При использовании нескольких экземпляров Slim Select поверх связанных <select>-элементов возникает задача поддержания согласованного состояния между ними. Состояние выбора может дублироваться, трансформироваться или частично зависеть от другого инстанса. Основной принцип реализации синхронизации заключается в наличии единого источника данных и управляемом распространении изменений между экземплярами без возникновения циклических обновлений.

Внутренне Slim Select оперирует массивом выбранных значений, а также набором доступных опций. Любое изменение выбора инициирует событие обновления, которое используется как триггер для синхронизации других компонентов.


Архитектура связанной системы экземпляров

Типовая схема синхронизации строится вокруг централизованного состояния:

  • единый объект состояния (store)
  • набор подписанных экземпляров Slim Select
  • обработчик изменений, распространяющий обновления
  • механизм подавления рекурсивных событий

Состояние обычно представлено структурой:

const sharedState = {
  country: null,
  state: null,
  city: null
};

Каждый экземпляр Slim Select получает привязку к этому состоянию через callback изменения.


Базовая синхронизация двух экземпляров

Наиболее простой сценарий — зеркалирование значений между двумя селектами.

const selectA = new SlimSelect({
  select: '#selectA',
  events: {
    afterChange: (info) => {
      syncFromA(info);
    }
  }
});

const selectB = new SlimSelect({
  select: '#selectB',
  events: {
    afterChange: (info) => {
      syncFromB(info);
    }
  }
});

Механизм синхронизации:

let lockA = false;
let lockB = false;

function syncFromA(info) {
  if (lockA) return;
  lockB = true;

  selectB.setSelected(info.map(i => i.value));

  lockB = false;
}

function syncFromB(info) {
  if (lockB) return;
  lockA = true;

  selectA.setSelected(info.map(i => i.value));

  lockA = false;
}

Использование флагов блокировки предотвращает зацикливание обновлений, возникающее при взаимной подписке.


Унификация состояния через внешний стор

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

const store = {
  value: null,
  subscribers: new Set(),

  setValue(val) {
    this.value = val;
    this.notify();
  },

  notify() {
    this.subscribers.forEach(fn => fn(this.value));
  },

  subscribe(fn) {
    this.subscribers.add(fn);
  }
};

Подключение Slim Select к стору:

const selectA = new SlimSelect({
  select: '#a',
  events: {
    afterChange: (info) => {
      store.setValue(info.map(i => i.value));
    }
  }
});

const selectB = new SlimSelect({
  select: '#b',
  events: {
    afterChange: (info) => {
      store.setValue(info.map(i => i.value));
    }
  }
});

Подписка экземпляров на обновления:

let internalUpdate = false;

store.subscribe((value) => {
  internalUpdate = true;

  selectA.setSelected(value);
  selectB.setSelected(value);

  internalUpdate = false;
});

Исключение повторной эмиссии событий достигается через флаг internalUpdate.


Синхронизация каскадных селектов

Часто используется иерархическая модель: страна → регион → город. В этом случае изменения одного уровня влияют на доступные значения следующего.

const countrySelect = new SlimSelect({
  select: '#country',
  events: {
    afterChange: (info) => updateStates(info[0]?.value)
  }
});

const stateSelect = new SlimSelect({
  select: '#state',
  events: {
    afterChange: (info) => updateCities(info[0]?.value)
  }
});

Обновление зависимых списков:

function updateStates(country) {
  const states = getStatesByCountry(country);

  stateSelect.setData(states);
  stateSelect.setSelected(null);

  citySelect.setData([]);
  citySelect.setSelected(null);
}

function updateCities(state) {
  const cities = getCitiesByState(state);

  citySelect.setData(cities);
  citySelect.setSelected(null);
}

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


Двунаправленная синхронизация с различными представлениями

Иногда несколько экземпляров отражают одно и то же состояние, но с разной фильтрацией или отображением.

Пример: один селект показывает все значения, второй — только избранные.

const allSelect = new SlimSelect({
  select: '#all',
  events: {
    afterChange: (info) => updateFavorites(info)
  }
});

const favSelect = new SlimSelect({
  select: '#fav',
  events: {
    afterChange: (info) => updateAll(info)
  }
});

Фильтрация состояния:

let state = {
  all: [],
  fav: []
};

function updateFavorites(info) {
  state.fav = info.map(i => i.value);

  favSelect.setSelected(state.fav);
}

function updateAll(info) {
  state.all = info.map(i => i.value);

  allSelect.setSelected(state.all);
}

При такой модели требуется аккуратное разделение ответственности, чтобы исключить конфликт источников данных.


Предотвращение циклических обновлений

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

1. Флаги блокировки

let isSyncing = false;

function handleChange(source, target, values) {
  if (isSyncing) return;

  isSyncing = true;
  target.setSelected(values);
  isSyncing = false;
}

2. Сравнение состояния перед обновлением

function arraysEqual(a, b) {
  return JSON.stringify(a) === JSON.stringify(b);
}

function safeUpdate(instance, values) {
  const current = instance.getSelected().map(i => i.value);

  if (!arraysEqual(current, values)) {
    instance.setSelected(values);
  }
}

3. Разделение событий пользователя и программных изменений

let programmatic = false;

selectA = new SlimSelect({
  select: '#a',
  events: {
    afterChange: (info) => {
      if (programmatic) return;
      sync(info);
    }
  }
});

function sync(info) {
  programmatic = true;
  selectB.setSelected(info.map(i => i.value));
  programmatic = false;
}

Синхронизация в режиме async-данных

При загрузке данных из API возникает дополнительная сложность: экземпляры могут обновляться не одновременно.

async function loadOptions() {
  const res = await fetch('/api/options');
  const data = await res.json();

  selectA.setData(data);
  selectB.setData(data);

  synchronizeInitialValue();
}

Согласование начального состояния:

function synchronizeInitialValue() {
  const initial = sharedState.value;

  if (initial) {
    selectA.setSelected(initial);
    selectB.setSelected(initial);
  }
}

Особое внимание требуется уделять моменту, когда события ещё не подписаны, но данные уже применяются.


Групповая синхронизация нескольких экземпляров

При большом количестве селектов используется модель регистрации:

const registry = [];

function register(instance) {
  registry.push(instance);
}

Обновление всех экземпляров:

function broadcast(value, origin) {
  registry.forEach(instance => {
    if (instance !== origin) {
      instance.setSelected(value);
    }
  });
}

Привязка:

const selectA = new SlimSelect({
  select: '#a',
  events: {
    afterChange: (info) => broadcast(info.map(i => i.value), selectA)
  }
});

register(selectA);

Синхронизация с частичным совпадением значений

В некоторых случаях наборы опций различаются, и требуется пересечение значений.

function intersectValues(sourceValues, targetInstance) {
  const available = targetInstance.data.map(i => i.value);
  return sourceValues.filter(v => available.includes(v));
}

Применение:

function syncPartial(values, target) {
  const valid = intersectValues(values, target);
  target.setSelected(valid);
}

Управление порядком обновлений

При сложных связях важно учитывать последовательность обновления экземпляров:

  1. источник события
  2. зависимые селекты
  3. вторичные синхронизации UI

Пример очередности:

function updateChain(value) {
  updateA(value);

  requestAnimationFrame(() => {
    updateB(value);

    setTimeout(() => {
      updateC(value);
    }, 0);
  });
}

Такой подход предотвращает блокировку UI и обеспечивает предсказуемость состояния.


Итоговая модель взаимодействия экземпляров

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

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