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-модели