Svelte
`$effect`: побочные эффекты
Канвас рисует график по данным, выбор темы надо сохранять в localStorage, заголовок вкладки должен показывать число непрочитанных. Всё это - выход за пределы реактивного графа во внешний мир: DOM, браузерные API, сеть. Для таких задач есть руна `$effect`. Она запускает функцию после того, как Svelte обновил DOM, и повторяет запуск, когда меняется любое реактивное состояние, прочитанное внутри. Но есть и обратная сторона: эффект - частый источник лишних перерисовок и циклов, если его применять не по назначению.
- Сохранение настроек в localStorage при изменении состояния через `$effect`
- Рисование на canvas или работа со сторонней библиотекой графиков, которой нужен готовый DOM
- Синхронизация заголовка вкладки или мета-тегов с состоянием приложения
- Подписки на события окна, веб-сокеты, таймеры с обязательной очисткой при размонтировании
- Логирование и аналитика: отправка события при изменении ключевого состояния
Предварительные знания
- Руны `$state` и `$derived`
- Понятие побочного эффекта: действие за пределами вычисления значения
- Знание браузерных API: localStorage, addEventListener, setInterval
Запуск эффекта и его зависимости
Руна `$effect` принимает функцию, которая выполняется после того, как Svelte обновил DOM. Внутри функции читают реактивное состояние, и Svelte запоминает, что именно прочитано. Когда любое из этих значений меняется, эффект запускается снова. Это место для синхронизации с внешним миром: запись в localStorage, работа с DOM-узлом, обращение к стороннему API.
Первый эффект читает theme и пишет его в localStorage - он перезапустится при смене темы. Второй читает count и обновляет заголовок вкладки - он перезапустится при изменении count. Список зависимостей нигде не объявляется руками: Svelte определяет его по тому, какое состояние эффект реально прочитал во время выполнения.
Отслеживание зависимостей по чтению означает: если значение прочитано внутри условия, которое не выполнилось, оно не станет зависимостью до того, как условие сработает. Эффект перезапускается только на изменения того состояния, которое он действительно использовал.
Когда запускается функция, переданная в `$effect`, и от чего зависит её повторный запуск?
Очистка и `$effect.pre`
Многие эффекты создают что-то, что нужно потом убрать: подписку на событие, интервал, веб-сокет. Для этого эффект может вернуть функцию очистки. Svelte вызовет её перед следующим запуском эффекта и при размонтировании компонента. Без очистки накапливаются висящие подписки и утечки памяти.
Эффект читает running, и при его изменении перезапускается. Перед перезапуском Svelte вызовет возвращённую функцию и очистит старый интервал, поэтому два таймера никогда не наложатся. Та же функция отработает при удалении компонента со страницы. Это стандартный шаблон для любых подписок.
| Форма | Когда выполняется |
|---|---|
| `$effect` | После обновления DOM |
| `$effect.pre` | До обновления DOM, перед перерисовкой |
| Возврат функции из эффекта | Перед следующим запуском и при размонтировании |
Форма `$effect.pre` нужна, когда действие должно произойти до того, как Svelte обновит DOM. Типичный случай - прочитать текущую позицию скролла или размеры элемента перед тем, как новые данные перерисуют список, чтобы потом восстановить позицию. В остальном `$effect.pre` ведёт себя как обычный эффект: те же зависимости и та же очистка.
Зачем эффект возвращает функцию, как в return () => clearInterval(id)?
Когда НЕ нужен эффект
Самая частая ошибка с эффектами - использовать их для вычисления состояния из другого состояния. Соблазн понятен: прочитать одно значение в эффекте и записать в другое. Но это создаёт лишний цикл и риск каскадных перерисовок, а порядок обновлений становится труднопредсказуемым. Если значение выводится из другого, это работа `$derived`, а не `$effect`.
Версия с эффектом создаёт total как `$state`, читает price и qty в эффекте и записывает результат. Это работает, но лишняя: появляется дополнительный проход реактивности и место, где значение можно случайно перезаписать. Версия с `$derived` короче, не имеет лишнего состояния и не может рассинхронизироваться. Эффект оставляют для настоящих побочных действий: запись в localStorage, DOM, сеть.
Запись в реактивное состояние внутри `$effect`, которое читает то же или связанное состояние, способна вызвать бесконечный цикл: эффект меняет значение, изменение перезапускает эффект, и так по кругу. Если тянет писать в состояние из эффекта, почти всегда нужен `$derived`.
- Вычислить значение из состояния - это `$derived`, не `$effect`
- Синхронизация с localStorage, DOM, сетью, таймерами - это `$effect`
- Подписки с обязательной очисткой - `$effect` с возвратом функции
- Действие до перерисовки (замер DOM) - `$effect.pre`
Почему вычислять total из price и qty лучше через `$derived`, а не записывать его в `$effect`?
Связь с другими темами
`$effect` - мост между реактивным состоянием и внешним миром:
- `$state`: реактивное состояние — Эффект запускается в ответ на изменение состояния `$state`
- `$derived`: производные значения — Для вычисления значения берут `$derived`, а не `$effect` - частая развилка
- Руны: явная реактивность — `$effect` отделил эффекты от вычислений, которые раньше смешивались в доллар-двоеточии
Итог
- Руна `$effect` запускает функцию-эффект после обновления DOM и повторяет её при изменении прочитанного внутри состояния
- Зависимости эффекта отслеживаются автоматически по тому, какое реактивное состояние он читает
- Возврат функции из эффекта задаёт очистку: она выполняется перед следующим запуском и при размонтировании
- Форма `$effect.pre` запускает эффект до обновления DOM, когда нужно прочитать состояние перед перерисовкой
- Эффекты не нужны для вычисления состояния: выводить значение из другого состояния - работа `$derived`, а не `$effect`
Связанные уроки
- sv-06-state — `$effect` реагирует на реактивное состояние, поэтому сначала нужен `$state`
- sv-07-derived — `$derived` и `$effect` оба следят за состоянием, но `$derived` для значений, а `$effect` для действий
- sv-05-runes-intro — `$effect` заменил эффект-часть метки доллар-двоеточие из Svelte 4