Перейти к содержимому

4. Реактивность

Реактивность Svelte: магия без лишнего кода ⚡

Заголовок раздела «Реактивность Svelte: магия без лишнего кода ⚡»

Привет! 👋 Яша здесь, и сегодня мы разберём главную суперсилу Svelte — реактивность. Если ты приходишь из мира React — готовься к культурному шоку. Здесь не нужен useState. Вообще. Никогда.

Просто напиши let count = 0 — и эта переменная уже реактивна. Магия!


В обычном JavaScript, если ты меняешь переменную — DOM ничего не знает. Он показывает то, что ему сказали при первой отрисовке, и всё. Хочешь обновить UI? Либо сам дёргай DOM, либо используй фреймворк с реактивностью.

В React ты делаешь так:

const [count, setCount] = useState(0);
// чтобы обновить:
setCount(count + 1);
// нельзя просто написать count = count + 1 😞

В Vue 3 так:

const count = ref(0);
// чтобы обновить:
count.value++;
// нельзя просто count++ — нужен .value 😅

А в Svelte… 🥁

<script>
let count = 0;
</script>
<button on:click={() => count++}>Нажми меня!</button>
<p>Счётчик: {count}</p>

Всё. let count = 0 — обычная JS-переменная, которая магически стала реактивной. Никаких хуков, никаких .value, никаких сеттеров. Просто переменная — и всё работает!


⚙️ Как это работает? Компилятор — вот в чём секрет

Заголовок раздела «⚙️ Как это работает? Компилятор — вот в чём секрет»

Svelte — это не библиотека, это компилятор. Когда Svelte видит твой .svelte файл, он анализирует код и генерирует оптимальный JavaScript.

Вот что примерно делает компилятор с кодом вида let count = 0:

// Псевдокод того, что генерирует компилятор Svelte:
let count = 0;
function $$invalidate(variable, value) {
// пересчитываем DOM — только те части, где используется переменная
}
// Когда ты пишешь count++, компилятор превращает это в:
// $$invalidate('count', ++count)

Это принципиально отличает Svelte от React и Vue:

  • React — Virtual DOM + reconciliation во время выполнения
  • Vue — Proxy-объекты + dep tracking во время выполнения
  • Svelte — статический анализ + генерация кода во время компиляции

Результат: минимальный JS-бандл + нет накладных расходов runtime + максимальная производительность.

💡 Если хочешь увидеть, что именно генерирует Svelte — зайди на svelte.dev/repl и переключи вкладку “JS Output”. Это очень поучительно!


📝 Простые присваивания — это всё, что нужно

Заголовок раздела «📝 Простые присваивания — это всё, что нужно»

Ключевое правило реактивности в Svelte: реактивность срабатывает при присваивании (=).

<script>
let name = 'Яша';
let count = 0;
function handleClick() {
count = count + 1; // ✅ реактивно — есть присваивание
count++; // ✅ тоже реактивно (это и есть count = count + 1)
count += 5; // ✅ и это тоже работает
count *= 2; // ✅ и это
name = 'Саша'; // ✅ строки тоже обновляются
}
</script>

Красота! Пишешь count++ — UI обновился. Никакого setCount.


Теперь поговорим об одной из моих любимых фишек Svelte — реактивных объявлениях!

<script>
let count = 0;
$: doubled = count * 2;
$: squared = count * count;
$: isEven = count % 2 === 0;
$: label = isEven ? 'чётное' : 'нечётное';
</script>
<p>Счётчик: {count}</p>
<p>Удвоенное: {doubled}</p>
<p>В квадрате: {squared}</p>
<p>Это {label} число</p>

Строки с $: — это реактивные объявления. Они работают как computed в Vue или useMemo в React, но намного проще: не нужно указывать массив зависимостей, Svelte сам всё определяет.

Как Svelte понимает зависимости? Статически, на этапе компиляции. Он видит, что doubled = count * 2 использует count — значит, пересчитывать doubled нужно каждый раз при изменении count.

💡 Откуда этот странный синтаксис $:? Это на самом деле метка (label) в JavaScript — легальный синтаксис, который просто нигде больше не используется. Svelte гениально переиспользовал его для реактивности. Компилятор видит метку $: и знает: “Это реактивное объявление!”


$: работает не только для вычисления значений, но и для запуска любого кода при изменении данных:

<script>
let count = 0;
// Реактивный console.log — запустится при каждом изменении count
$: console.log('count изменился:', count);
// Реактивный if
$: if (count > 10) {
alert('Слишком много!');
count = 10; // можно менять переменные прямо здесь!
}
// Реактивный вызов функции
$: document.title = `Счётчик: ${count}`;
// Реактивная функция
$: updateAnalytics(count);
function updateAnalytics(value) {
console.log('Отправляем в аналитику:', value);
}
</script>

Это очень мощная штука. По сути, $: — это универсальный инструмент: и для вычисляемых значений, и для сайд-эффектов.

В React ты бы написал:

useEffect(() => {
console.log('count изменился:', count);
}, [count]); // не забудь зависимости!

В Svelte:

$: console.log('count изменился:', count);

Одна строка. Зависимости определяются автоматически. ✨


Можно группировать несколько реактивных операций в блок:

<script>
let width = 10;
let height = 20;
$: {
// Этот блок перезапустится при изменении width ИЛИ height
console.log('Размеры изменились!');
console.log('Ширина:', width, 'Высота:', height);
validateDimensions(width, height);
}
$: area = width * height;
$: perimeter = 2 * (width + height);
function validateDimensions(w, h) {
if (w <= 0 || h <= 0) {
console.warn('Размеры должны быть положительными!');
}
}
</script>

Всё, что находится в блоке $: { ... }, будет перезапускаться при изменении любой переменной, которая в нём используется.


🧩 Порядок реактивных объявлений: Svelte разберётся сам

Заголовок раздела «🧩 Порядок реактивных объявлений: Svelte разберётся сам»

Вот где Svelte показывает свою настоящую силу. Посмотри на этот код:

<script>
let count = 0;
// Не важно, в каком порядке написаны!
$: quadrupled = doubled * 2; // использует doubled...
$: doubled = count * 2; // ...которое ещё не объявлено выше!
</script>

В обычном JavaScript это бы не сработало — doubled используется до объявления. Но Svelte анализирует зависимости и автоматически выстраивает правильный порядок выполнения.

Сравни с React:

// В React нужно следить за порядком сам!
const doubled = useMemo(() => count * 2, [count]);
const quadrupled = useMemo(() => doubled * 2, [doubled]); // doubled должен быть первым!

🚧 Важное ограничение: мутации vs переприсваивание

Заголовок раздела «🚧 Важное ограничение: мутации vs переприсваивание»

Вот тут начинается самый важный нюанс Svelte. Слушай внимательно! 👂

Для массивов нужно переприсваивание:

<script>
let fruits = ['яблоко', 'банан'];
function addFruit() {
// ❌ НЕ РАБОТАЕТ — нет присваивания!
fruits.push('вишня');
// ✅ РАБОТАЕТ — создаём новый массив
fruits = [...fruits, 'вишня'];
// ✅ ТОЖЕ РАБОТАЕТ — "self-assignment хак"
fruits.push('вишня');
fruits = fruits; // <-- это триггерит реактивность
}
function removeFruit(index) {
// ❌ splice мутирует массив — не реактивно
fruits.splice(index, 1);
// ✅ filter создаёт новый массив — реактивно
fruits = fruits.filter((_, i) => i !== index);
}
function updateFruit(index, newValue) {
// ❌ Прямое изменение элемента
fruits[index] = newValue;
// ✅ Через map
fruits = fruits.map((f, i) => i === index ? newValue : f);
// ✅ Или self-assignment хак
fruits[index] = newValue;
fruits = fruits;
}
</script>

Для объектов всё интереснее:

<script>
let user = { name: 'Яша', age: 25, address: { city: 'Москва' } };
function birthday() {
// ✅ РАБОТАЕТ для объектов! Svelte отслеживает свойства
user.age = user.age + 1;
}
function changeCity() {
// ✅ Даже вложенные свойства работают!
user.address.city = 'Питер';
}
function updateUser() {
// ✅ Полное переприсваивание тоже работает
user = { ...user, name: 'Саша', age: 30 };
}
</script>

Почему так? Потому что user.age = val — это всё равно присваивание с точки зрения JavaScript-синтаксиса. А array.push() — это вызов метода, который мутирует массив без присваивания.

Svelte следит за присваиваниями, а не за мутациями. Это его принципиальное ограничение, о котором важно знать.


Вот шпаргалка типичных операций с массивами в Svelte:

<script>
let items = ['один', 'два', 'три'];
let newItem = '';
// ✅ Добавить в конец
const push = (item) => items = [...items, item];
// ✅ Добавить в начало
const unshift = (item) => items = [item, ...items];
// ✅ Удалить по индексу
const remove = (i) => items = items.filter((_, idx) => idx !== i);
// ✅ Обновить элемент
const update = (i, val) => items = items.map((item, idx) => idx === i ? val : item);
// ✅ Отсортировать (sort мутирует — создаём копию!)
const sort = () => items = [...items].sort();
// ✅ Очистить
const clear = () => items = [];
// ✅ Самый краткий способ через self-assignment
const pushHack = (item) => { items.push(item); items = items; };
</script>

💡 Совет: команда Svelte рекомендует иммутабельный подход (через spread и filter) как более читаемый. Self-assignment хак arr = arr работает, но выглядит немного магически.


<script>
let settings = {
theme: 'dark',
language: 'ru',
notifications: {
email: true,
push: false
}
};
// ✅ Обновить поверхностное свойство — работает напрямую!
settings.theme = 'light';
// ✅ Обновить вложенное — тоже работает
settings.notifications.email = false;
// ✅ Иммутабельный способ (когда нужна гарантия)
settings = { ...settings, theme: 'light' };
// ✅ Обновить вложенный объект иммутабельно
settings = {
...settings,
notifications: { ...settings.notifications, email: false }
};
</script>

Для объектов Svelte достаточно умён, чтобы отследить изменения вложенных свойств. Но если хочешь гарантированно точечного обновления — используй иммутабельный подход.


В Svelte 5 появился совершенно новый API реактивности — руны (runes). Это специальные функции-компиляторы:

<script>
// Svelte 5: $state() вместо обычного let
let count = $state(0);
// $derived() вместо $:
let doubled = $derived(count * 2);
let squared = $derived(count * count);
let isEven = $derived(count % 2 === 0);
// $effect() вместо $: для сайд-эффектов
$effect(() => {
console.log('count изменился:', count);
// Автоматически запускается снова при изменении зависимостей
// Автоматически очищает предыдущий эффект
});
// $effect.pre() — запускается ДО обновления DOM
$effect.pre(() => {
console.log('Перед обновлением DOM:', count);
});
</script>

Зачем нужны руны, если let и $: уже работают?

Руны дают несколько преимуществ:

  • Явность: сразу видно, что переменная реактивная
  • Переносимость: работают в обычных .ts и .js файлах, не только в .svelte
  • Предсказуемость: более консистентное поведение в edge cases
  • Лучший инструментарий: TypeScript видит тип $state<T>()
// Svelte 5 в обычном .ts файле!
export const count = $state(0); // ✅ работает вне .svelte
export const doubled = $derived(count * 2);
// В React такого нет — хуки только внутри компонентов
const [count, setCount] = useState(0); // ❌ только в компонентах

ЗадачаSvelte 4Svelte 5ReactVue 3
Объявить переменнуюlet x = 0let x = $state(0)const [x, setX] = useState(0)const x = ref(0)
Обновитьx = 1x = 1setX(1)x.value = 1
Вычисляемое$: y = x * 2let y = $derived(x * 2)const y = useMemo(() => x*2, [x])const y = computed(() => x.value*2)
Эффект$: doSomething(x)$effect(() => { ... })useEffect(() => {...}, [x])watchEffect(() => {...})
ЗависимостиАвтоматическиАвтоматическиВручнуюАвтоматически

Svelte выигрывает по краткости в большинстве случаев. А ещё — никаких правил хуков, никаких “stale closure” багов, никаких случайно забытых зависимостей в массиве. 🏆


1. Забыть переприсвоить массив:

// ❌ Не обновит UI
function add(item) {
myArray.push(item);
}
// ✅ Правильно
function add(item) {
myArray = [...myArray, item];
}

2. Зависимость от внешней переменной в $::

// ❌ count снаружи — Svelte не отследит изменения
let count = externalModule.count;
$: doubled = count * 2; // не обновится при изменении externalModule.count
// ✅ Используй реактивную переменную
$: count = externalModule.count;
$: doubled = count * 2;

3. Асинхронные обновления:

async function loadData() {
const data = await fetch('/api/data').then(r => r.json());
// ✅ Это работает! Svelte отследит присваивание
myData = data;
}

Async/await не ломает реактивность. Присваивание после await всё равно триггерит обновление. ✅


Хватит теории — время практики! Меняй счётчик и наблюдай за реактивными зависимостями, логом и массивом в реальном времени: