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: новые руны (runes)
Заголовок раздела «🔮 Svelte 5: новые руны (runes)»В 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); // ✅ работает вне .svelteexport const doubled = $derived(count * 2);
// В React такого нет — хуки только внутри компонентовconst [count, setCount] = useState(0); // ❌ только в компонентах📊 Сравнение: Svelte vs React vs Vue
Заголовок раздела «📊 Сравнение: Svelte vs React vs Vue»| Задача | Svelte 4 | Svelte 5 | React | Vue 3 |
|---|---|---|---|---|
| Объявить переменную | let x = 0 | let x = $state(0) | const [x, setX] = useState(0) | const x = ref(0) |
| Обновить | x = 1 | x = 1 | setX(1) | x.value = 1 |
| Вычисляемое | $: y = x * 2 | let 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. Забыть переприсвоить массив:
// ❌ Не обновит UIfunction 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 всё равно триггерит обновление. ✅
🎮 Интерактивный Playground
Заголовок раздела «🎮 Интерактивный Playground»Хватит теории — время практики! Меняй счётчик и наблюдай за реактивными зависимостями, логом и массивом в реальном времени: