Руны Svelte 5: почему фреймворк отказался от «магического» let
Интерфейс Евы написан на Svelte, а в пятой версии фреймворк серьёзно поменял реактивность. Разбираю пост команды Svelte, в котором они объясняют зачем.
Как было
В Svelte 3–4 реактивность была почти магической:
<script>
let count = 0; // уже реактивно
$: double = count * 2; // пересчитывается само
</script>Это красиво, пока проект маленький. Потом начинаются проблемы:
- Работает только на верхнем уровне компонента. Вынес логику в обычный
.jsфайл — реактивность пропала. Рефакторинг превращался в переписывание. - Зависимости
$:определялись при компиляции. Если значение читается внутри вызова функции, компилятор этого не видит, и обновление теряется. - Сторы для общего состояния требовали отдельного API с подписками,
$-префиксами и своими правилами.
Как стало
Руны — это специальные символы с синтаксисом функций, которые понимает компилятор:
<script>
let count = $state(0);
let double = $derived(count * 2);
$effect(() => {
console.log(`count = ${count}`);
});
let { title } = $props(); // вместо export let
</script>$stateобъявляет реактивное состояние;$derivedобъявляет вычисляемое значение;$effectвыполняет побочный эффект при изменении зависимостей;$propsполучает свойства компонента.
Главное: руны работают и в .svelte.js файлах, а не только в компонентах. Общее состояние больше не требует сторов.
Что под капотом
Внутри Svelte 5 — сигналы: идея, которая была ещё в Knockout в 2010 году и которую популяризировал Solid. Каждое значение знает, кто его читает. При изменении обновляются ровно те места, которые от него зависят. Зависимости отслеживаются во время выполнения, поэтому вызов через функцию больше не ломает реактивность.
Бонус — мелкозернистые обновления: если в списке из тысячи элементов поменялся один, обновится только он.
Мой комментарий
Для монитора планировщика Евы это ровно то, что нужно. Статусы узлов DAG приходят потоком, и каждый узел должен перерисовываться отдельно, не трогая остальной граф. С рунами состояние графа живёт в отдельном модуле .svelte.ts, а компоненты просто его читают. Явное лучше неявного: $state(0) сразу говорит, что это реактивно, без знания особых правил для let.