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 делает реактивными чтения времени, что удобно для таймеров и часов.
| Нужна структура | Реактивный вариант | Откуда |
|---|---|---|
| Множество уникальных значений | SvelteSet | svelte/reactivity |
| Пары ключ-значение | SvelteMap | svelte/reactivity |
| Дата и время | SvelteDate | svelte/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` внутри даёт вычисляемое поле рядом с состоянием