State Management

Observable и proxy-реактивность

В reducer-модели, чтобы увеличить счётчик, нужно объявить action, написать его обработку в редьюсере, вернуть новое состояние иммутабельно и сделать dispatch. Observable/proxy-парадигма спрашивает: а что, если просто написать state.count++, как у обычного объекта, а система сама заметит, что count изменился, и обновит только те компоненты, которые этот count читали? Никаких action, dispatch и ручных спредов. Именно так работают MobX, реактивность Vue и Valtio: мутируем привычно, а отслеживание зависимостей и обновление UI берёт на себя библиотека.

  • Vue и Pinia, где state.count++ автоматически обновляет связанный шаблон
  • MobX в формах и сложных моделях домена, где явная reducer-обвязка ощущается тяжёлой
  • Valtio для глобального стора, к которому обращаются как к обычному объекту
  • Редакторы и канвасы, где удобно менять вложенные поля модели напрямую
  • Команды, выбравшие минимум церемоний: мутируй как обычно, реактивность сама подхватит

Предварительные знания

  • Обзор парадигм реактивности и место observable/proxy на карте
  • Понимание, что компонент подписывается на данные и обновляется при их изменении
  • Базовое знакомство с тем, что такое Proxy в JavaScript на уровне идеи
  • Парадигмы: обзор

Мутируем как обычно

Observable/proxy-парадигма строится на простой эргономике: состояние выглядит и меняется как обычный объект. Чтобы увеличить счётчик, пишут state.count++. Чтобы добавить в список, пишут state.items.push(x). Никаких action-объектов, dispatch и иммутабельных спредов. Реактивная система сама замечает изменение и обновляет зависимый UI. Это прямая противоположность reducer-модели по стилю написания.

  • Reducer-модель — Объявить action, обработать в редьюсере, вернуть новое состояние иммутабельно, сделать dispatch. Больше кода, зато явный прослеживаемый поток.
  • Observable/proxy — Просто state.count++. Реактивность сама отследит зависимость и обновит UI. Меньше церемоний, но изменение неявное и не проходит через единую точку.

Удобство мутации это и сила, и слабость парадигмы. Сила в том, что кода почти нет. Слабость в том, что изменения не проходят через единую явную точку, поэтому встроенная прослеживаемая история действий, как в reducer-модели, здесь не возникает сама собой.

Чем стиль изменения состояния в observable/proxy-парадигме отличается от reducer-модели?

Автоматическое отслеживание зависимостей

Главный механизм парадигмы это автоматическое отслеживание зависимостей. Библиотека оборачивает состояние в Proxy - объект, перехватывающий обращения к полям. Когда компонент во время рендера читает поле, proxy это фиксирует: значит, компонент зависит от этого поля. Когда позже в это поле пишут, библиотека знает точный список зависящих от него потребителей и обновляет только их.

Главное следствие - точечность без ручных селекторов. В reducer-модели, чтобы избежать лишних ре-рендеров, разработчик пишет узкие селекторы вручную. Здесь подписка на конкретные поля возникает автоматически из того, что компонент реально прочитал во время рендера. Прочитал только count - обновится только при изменении count, даже если рядом в том же объекте поменялось другое поле.

Производные значения в этой парадигме тоже отслеживаются автоматически: computed в Vue и MobX перечитывается только тогда, когда меняется хоть одна из его зависимостей. Разработчик не указывает зависимости списком - они выводятся из того, какие поля computed прочитал при вычислении.

Практическое следствие автотрекинга: важно именно читать поле там, где нужна реактивность. Если значение прочитано вне отслеживаемого контекста (например один раз сохранено в переменную заранее), зависимость может не зафиксироваться, и обновление не придёт.

Как observable/proxy-система узнаёт, какие компоненты обновить при изменении поля?

Снимки состояния

Мутабельное проксированное состояние удобно менять, но для рендера и сравнения нужен устойчивый неизменяемый срез. Эту роль играет снимок (snapshot). Снимок это иммутабельная фотография состояния на текущий момент. Компонент читает из снимка, и пока состояние не изменилось, снимок остаётся той же ссылкой, что и позволяет дёшево сравнивать по ссылке.

Разделение ролей здесь чёткое: пишут в мутабельный проксированный объект (state), читают в рендере из иммутабельного снимка (snap). Новая мутация порождает новый снимок только для затронутых частей, поэтому нетронутые ветки сохраняют прежние ссылки, и зависящие от них компоненты не ре-рендерятся. Это сочетает удобство мутации с дешёвым обнаружением изменений по ссылке.

АспектReducer-модельObservable/proxy
ИзменениеAction + dispatchПрямая мутация поля
Подписка на частиРучные селекторыАвтотрекинг прочитанных полей
ИммутабельностьПишет разработчикСнимок даёт библиотека
История действийВстроена через поток actionНе возникает сама собой

Снимок предназначен только для чтения. Менять надо мутабельный проксированный объект, а не снимок: попытка записать в snapshot не изменит источник истины и приведёт к рассогласованию между тем, что показано, и тем, что хранится.

Зачем в observable/proxy-парадигме нужен снимок (snapshot)?

Связь с другими темами

Proxy-реактивность противопоставлена reducer-модели и лежит в основе Vue:

  • Flux и reducer-модель — Противоположный полюс: явные action и иммутабельность против привычной мутации и автотрекинга
  • Pinia — Стор Vue построен на этой же proxy-реактивности: меняем поля напрямую, шаблон обновляется сам

Итог

  • Observable/proxy-парадигма позволяет менять состояние привычной мутацией, без action и dispatch
  • Библиотека оборачивает состояние в Proxy и перехватывает чтения и записи полей
  • Во время рендера фиксируется, какие именно поля прочитал компонент - это его зависимости
  • При записи в поле обновляются только те потребители, что читали именно это поле - точечная реактивность
  • Снимок (snapshot) даёт неизменяемый срез состояния для безопасного чтения и сравнения по ссылке
  • Так устроены MobX, реактивность Vue и Pinia, а также Valtio; парадигма противоположна reducer-модели по эргономике

Связанные уроки

  • sm-06-paradigms-overview — Observable/proxy это вторая парадигма из общей карты, теперь подробно
  • sm-07-flux-reducer — Противоположный полюс: явные действия и редьюсеры против привычной мутации с автотрекингом
  • vue-27-pinia-intro — Реактивность Vue и Pinia построена ровно на этой proxy-модели
Observable и proxy-реактивность

0

1

Войти