Знакомство с рунами / Хабр
Эта статья — перевод оригинальной статьи “Introducing runes”.
Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.
Вступление
В 2019 году Svelte 3 превратил JavaScript в реактивный язык. Svelte – это фреймворк для создания веб-интерфейса, который использует компилятор для превращения декларативного кода компонентов в такой…
…в жестко оптимизированный JavaScript, который обновляет документ при изменении состояния, например, count. Поскольку компилятор “видит”, где ссылаются на count, генерируемый код очень эффективен, а поскольку мы используем такие синтаксисы, как let и =, а не громоздкие API, вы можете писать меньше кода.
Чаще всего мы получаем такие отзывы: “Хотел бы я писать весь свой JavaScript именно так”. Когда вы привыкли к тому, что вещи внутри компонентов волшебным образом обновляются, возврат к старому скучному процедурному коду кажется вам переходом от цветного к черно-белому.
В Svelte 5 все это изменилось благодаря рунам, которые открывают универсальную, тонкую реактивность.
Прежде чем начать
Несмотря на то, что мы меняем принцип работы под капотом, Svelte 5 должен стать полноценной заменой практически для всех. Новые возможности являются опциональными – существующие компоненты будут продолжать работать.
Дата выхода Svelte 5 пока не определена. То, что мы показываем здесь, – это наработки, которые, могут измениться.
Что такое руны?
Руны – это символы, влияющие на работу компилятора Svelte. Если сегодня в Svelte для обозначения конкретных вещей используются let, =, ключевое слово export и метка $:, то руны используют синтаксис функций для достижения того же и даже большего.
Например, чтобы объявить часть реактивного состояния, мы можем использовать руну $state:
На первый взгляд, это может показаться шагом назад – возможно, даже не по-свельтовски. Не лучше ли, если let count будет реактивным по умолчанию?
Нет. Реальность такова, что по мере роста сложности приложений выяснение того, какие значения являются реактивными, а какие нет, может стать сложной задачей. К тому же эвристика работает только для объявлений let на верхнем уровне компонента, что может привести к путанице. Если в файлах .svelte код ведет себя одним образом, а в .js – другим, это может затруднить рефакторинг кода, например, если вам нужно превратить что-то в хранилище, чтобы использовать его в нескольких местах.
За пределами компонентов
С помощью рун реактивность выходит за пределы файлов .svelte. Предположим, мы хотим инкапсулировать логику счетчика таким образом, чтобы ее можно было повторно использовать в разных компонентах. Сегодня для этого используется кастомное хранилище в файле .js или .ts:
import { writable } from ‘svelte/store’;
export function createCounter() {
const { subscribe, update } = writable(0);
return {
subscribe,
increment: () => update((n) => n + 1)
};
}
Поскольку в данном случае реализуется контракт с хранилищем – возвращаемое значение имеет метод subscribe, – мы можем ссылаться на значение магазина, добавляя к его имени префикс $:
//
Это работает, но довольно странно! Мы обнаружили, что API хранилища может стать довольно громоздким, когда вы начинаете делать более сложные вещи.
С рунами все гораздо проще:
// import { writable } from ‘svelte/store’;
export function createCounter() {
// const { subscribe, update } = writable(0);
let count = $state(0);
return {
// subscribe,
// increment: () => update((n) => n + 1)
get count() { return count },
increment: () => count += 1
};
}
// clicks: {$counter}
clicks: {counter.count}
Обратите внимание, что мы используем свойство get в возвращаемом объекте, поэтому counter.count всегда ссылается на текущее значение, а не на значение в момент вызова функции.
Реактивность во время выполнения
Сегодня Svelte использует реактивность во время компиляции. Это означает, что если у вас есть код, использующий метку $: для автоматического перезапуска при изменении зависимостей, то эти зависимости будут определены при компиляции компонента в Svelte:
Это работает хорошо… до того момента пока не перестанет. Предположим, что мы рефакторим приведенный выше код:
const multiplyByHeight = (width) => width * height;
$: area = multiplyByHeight(width);
Поскольку объявление $: area = … может “видеть” только width, оно не будет пересчитываться при изменении height. В результате код трудно рефакторить, а понимание тонкостей того, когда Svelte решает обновить те или иные значения, может стать довольно сложным после определенного уровня сложности.
В Svelte 5 появились руны $derived и $effect, которые вместо этого определяют зависимости своих выражений при их вычислении:
Как и $state, $derived и $effect также могут быть использованы в файлах .js и .ts.
Усиление сигнала
Как и любой другой фреймворк, мы пришли к пониманию того, что Knockout всегда был прав.
Реактивность Svelte 5 обеспечивается сигналами, которые, по сути, являются тем, чем занимался Knockout в 2010 году. Совсем недавно сигналы были популяризированы Solid и приняты множеством других фреймворков.
Однако у нас все немного по-другому. В Svelte 5 сигналы – это детали реализации, а не то, с чем вы взаимодействуете напрямую. Таким образом, мы не имеем тех же ограничений на дизайн API и можем максимально повысить эффективность и эргономичность. Например, мы избегаем проблем с сужением типов, возникающих при обращении к значениям через вызов функции, а при компиляции в режиме рендеринга на стороне сервера мы можем вообще отказаться от сигналов, поскольку на сервере они – не более чем накладные расходы.
Сигналы позволяют реализовать мелкозернистую реактивность, то есть, например, изменение значения в большом списке не обязательно должно приводить к аннулированию всех остальных членов списка. Таким образом, Svelte 5 работает невероятно быстро.
Впереди более простые времена
Руны – это добавочное свойство, но они делают устаревшими целую кучу существующих концепций:
разница между let на верхнем уровне компонента и в остальных местах
export let
$:, со всеми вытекающими отсюда странностями
разное поведение между
0