Svelte

Паттерны состояния: классы и реактивные коллекции

Корзина обрастает логикой: добавить товар, убрать, посчитать сумму, применить скидку. Раскидывать это по отдельным переменным и функциям - значит развязать состояние и поведение, которые должны жить вместе. Поля класса в Svelte 5 объявляются через руну `$state`, поэтому корзина становится обычным объектом класса: items внутри реактивно, а метод add и поле total лежат рядом. Создаётся такой объект через new, передаётся по приложению, и каждое изменение поля перерисовывает зависящую разметку.

  • Класс Cart с реактивными полями items и геттером total - вся логика корзины в одном месте
  • Класс формы с полями значений и методом validate, собранными вместе
  • Реестр открытых вкладок через SvelteSet: добавление и удаление id реактивны
  • Кэш ответов по ключу через SvelteMap: запись в кэш сразу видна в интерфейсе
  • Таймер обратного отсчёта на SvelteDate: текущее время реактивно обновляет отображение

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

  • Руна `$state` и глубокая реактивность объектов
  • JavaScript-классы: поля, методы, геттеры, ключевое слово this
  • Знание встроенных Map и Set и их методов

Поля класса через `$state`

Руна `$state` объявляет не только переменные, но и поля класса. Поле, инициализированное через `$state`, становится реактивным: его изменение через this обновляет разметку, где это поле читают. Состояние и методы, которые его меняют, оказываются в одном объекте. Это естественный способ собрать сложную единицу состояния - корзину, форму, плеер - в инкапсулированную сущность.

Поле items реактивно, метод add мутирует его через push, и зависящая разметка обновляется. Поле total объявлено через `$derived` и пересчитывается, как только меняется items: вычисляемое поле живёт прямо рядом с состоянием. Экземпляр создаётся через new Cart и используется как обычный объект.

В обработчиках событий метод вызывают как cart.add, и this внутри указывает на экземпляр. Если метод передают как callback в другое место, привязку this стоит проверять - стрелочные методы или явный bind сохраняют контекст экземпляра.

Что делает поле класса, инициализированное через `$state`?

Инкапсуляция состояния в классе

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

Поле #value скрыто: снаружи его нельзя ни прочитать напрямую, ни записать. Геттер value отдаёт значение только на чтение, а методы increment и reset задают единственные способы его менять. Реактивность сохраняется и через геттер - чтение this.#value в нём отслеживается так же, как прямое.

  • Разрозненные переменные и функции — Состояние в одних местах, меняющие его функции в других. Легко изменить значение в обход правил и получить недопустимое состояние
  • Класс с инкапсуляцией — Состояние приватно, изменения идут только через методы. Допустимые переходы заданы в одном месте, обойти их нельзя

Класс - это одна ответственность на одну сущность. Корзина управляет товарами, форма управляет полями и валидацией. Не стоит сваливать в один класс несвязанные области состояния - это возвращает высокую связанность, от которой инкапсуляция как раз уводит.

Зачем прятать состояние в приватное поле и отдавать доступ через методы?

Реактивные коллекции из svelte/reactivity

Глубокий прокси `$state` оборачивает обычные объекты и массивы, но встроенные Map, Set и Date он реактивными не делает. Их внутреннее состояние спрятано за методами, и прокси до него не дотягивается: вызов map.set или set.add не вызовет обновления интерфейса. Для этих случаев пакет svelte/reactivity даёт готовые реактивные версии: SvelteMap, SvelteSet и SvelteDate.

SvelteSet используется как обычный Set: те же add, delete, has, size. Разница в том, что эти операции реактивны - openTabs.size в разметке обновляется при каждом изменении. SvelteMap работает так же для пар ключ-значение, а SvelteDate делает реактивными чтения времени, что удобно для таймеров и часов.

Нужна структураРеактивный вариантОткуда
Множество уникальных значенийSvelteSetsvelte/reactivity
Пары ключ-значениеSvelteMapsvelte/reactivity
Дата и времяSvelteDatesvelte/reactivity
Объект или массивОбычный `$state`встроено

Класть обычный Map в `$state` бессмысленно: глубокий прокси не отслеживает его внутренние мутации, и интерфейс не обновится при map.set. Когда нужна реактивная пара ключ-значение, берут именно SvelteMap из svelte/reactivity, а не обычный Map в обёртке `$state`.

Почему обычный Set, помещённый в `$state`, не обновляет интерфейс при вызове add?

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

Этот урок - про организацию реактивного состояния в объекты:

  • `$state`: реактивное состояние — Поле класса объявляется той же руной
  • Универсальная реактивность — Экземпляр класса - удобная единица общего состояния в модуле
  • `$derived`: производные значения — Геттер класса с `$derived` даёт вычисляемое поле

Итог

  • Поля класса можно объявлять через `$state` - экземпляр становится реактивным объектом с состоянием и методами вместе
  • Геттер класса с `$derived` внутри даёт вычисляемое поле, которое пересчитывается при изменении зависимостей
  • Класс инкапсулирует состояние: снаружи доступны методы, а внутреннее устройство скрыто
  • Обычные Map и Set не реактивны - их мутации не вызывают обновления интерфейса
  • svelte/reactivity даёт SvelteMap, SvelteSet и SvelteDate, которые реактивны при тех же привычных методах

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

  • sv-06-state — Поля класса используют ту же руну `$state`, что и обычное реактивное состояние
  • sv-10-universal-reactivity — Класс - удобная упаковка для общего состояния, вынесенного в модуль .svelte.js
  • sv-07-derived — Геттер класса с `$derived` внутри даёт вычисляемое поле рядом с состоянием
Паттерны состояния: классы и реактивные коллекции

0

1

Войти