
Даже если вы никогда не заглядывали «под капот» фреймворков, Svelte 5 — отличный повод это сделать. Вместо сухих теорий мы шаг за шагом разберём, как привычный HTML-подобный код Svelte превращается в быстрый JavaScript, способный работать без лишнего балласта. Автор статьи, Тан Ли Хау из сообщества Svelte, показывает этот процесс на простых примерах, так что вы сможете буквально «скомпилировать» Svelte у себя в голове и понять, как на самом деле работает ваш код.
Давайте освежим в памяти, как мы пишем веб-приложения без использования фреймворков:
// создаём элемент h1
const h1 = document.createElement('h1');
// создаём text node
const text = document.createTextNode('Hello World');
h1.appendChild(text);
// ...и добавляем это к body
document.body.appendChild(h1);
// обновление текста в ноде
text.nodeValue = 'Bye World';
// добавляем название класса к элементу h1
h1.setAttribute('class', 'abc');
// ...и добавление <style> тэг к head
const style = document.createElement('style');
style.textContent = '.abc { color: blue; }';
document.head.appendChild(style);
// удаление элемента
document.body.removeChild(h1);
const button = document.createElement('button');
button.textContent = 'Click Me!';
// прослушиваем событие "click"
button.addEventListener('click', () => {
console.log('Hi!');
});
document.body.appendChild(button);
Это код, который вы писали бы, не используя никаких фреймворков или библиотек.
В предыдущих версиях Svelte этого знания DOM было бы достаточно, чтобы провести параллели с Svelte.
Однако в Svelte 5 была применена более глубокая оптимизация. Прежде чем мы начнём говорить о фреймворке, нам нужно изучить несколько более продвинутых концепций DOM, таких как <template>
и делегирование событий.
<temlate> element
<template> — это специальный элемент HTML, позволяющий определить фрагмент HTML, который не отображается. Вместо этого его содержимое хранится в виде «шаблона», который можно клонировать и впоследствии вставить в DOM с помощью JavaScript.
Такое решение полезно для создания повторно используемых структур DOM:
// создаём template element
const template = document.createElement('template');
template.innerHTML = '<h1>Hello World</h1>';
// клониурем template
const h1 = template.cloneNode(true).content.firstChild;
// вставляем склонированный template в body
document.body.appendChild(h1);
Например, если ваш компонент содержит несколько более сложную структуру элементов, может быть более эффективно клонировать его из <template>
, чем вручную перестраивать структуру DOM с помощью createElement.
nextSibling and firstChild
Когда вы клонируете элементы DOM с помощью <template>
, полезно получить ссылку на конкретные из них внутри клонированного.
const template = document.createElement('template');
template.innerHTML = '<h1>Hello <span>World</span></h1>';
const h1 = template.cloneNode(true).content.firstChild;
// get the first child of the cloned element
const text = h1.firstChild; // "Hello "
// get the next sibling of the first child
const span = text.nextSibling; // <span>World</span>
Делегирование событий
Когда у вас много элементов, которые вы хотите прослушивать на одно и то же имя события, например, событие «click»
, вы можете использовать технику «делегирование событий». То есть прослушивать событие на общем родительском элементе, а не добавлять слушателей событий на каждый элемент.
Например, в следующей структуре HTML:
<div>
<button>Click me</button>
<button>Click me</button>
</div>
Вместо добавления слушателей событий на каждую кнопку:
button1.addEventListener('click', () => {
console.log('Button 1 clicked');
});
button2.addEventListener('click', () => {
console.log('Button 2 clicked');
});
Вы можете прослушать событие на родительском <div>
:
div.addEventListener('click', (event) => {
if (event.target === button1) {
console.log('Button 1 clicked');
} else if (event.target === button2) {
console.log('Button 2 clicked');
}
});
Делегирование событий — это распространенная техника, используемая в большинстве фреймворков JavaScript, поскольку она более эффективна с точки зрения производительности и гораздо проще в очистке.
Вот пара ресурсов, которые я нашел полезными для изучения делегирования событий:
learn.javascript.ru: Делегирование событий
После достаточно подробного введения в DOM, пришло время поговорить о Svelte 5.
Синтаксис Svelte
Здесь мы рассмотрим некоторые основы синтаксиса Svelte 5.
Если вы хотите узнать больше, я настоятельно рекомендую попробовать интерактивный учебник Svelte.
Вот базовый компонент Svelte (Svelte REPL):
<h1>Hello World</h1>
Чтобы добавить стиль, добавьте тег <style>
(Svelte REPL):
<style>
h1 {
color: rebeccapurple;
}
</style>
<h1>Hello World</h1>
На данный момент написание компонента Svelte похоже на написание HTML, потому что синтаксис Svelte является надмножеством синтаксиса HTML.
Давайте посмотрим, как мы добавляем данные в наш компонент с помощью рун Svelte (Svelte REPL):
<script>
let name = $state('World');
</script>
<h1>Hello {name}</h1>
Руны — это своего рода маркеры, которые сообщают компилятору Svelte о вашем коде. Мы используем руну $state
, чтобы пометить переменную name как реактивное состояние.
Чтобы использовать реактивное состояние в шаблоне, мы можем указать его внутри фигурных скобок {}
. Внутри них мы можем включить любое выражение JavaScript.
Чтобы добавить обработчик клика, мы пишем атрибут onclick
в элементе button
, так же, как вы делаете это в HTML (Svelte REPL):
<script>
let count = $state(0);
function onClickButton(event) {
console.log(count);
}
</script>
<button onclick={onClickButton}>Clicked {count}</button>
Чтобы изменить значение реактивного состояния, мы просто присваиваем ему значение (Svelte REPL):
<script>
let count = $state(0);
function onClickButton(event) {
count += 1;
}
</script>
<button onclick={onClickButton}>Clicked {count}</button>
Поскольку мы пометили count как реактивное состояние с помощью руны $state
, любое изменение значения переменной будет отражено в DOM.
Компиляция Svelte 5
Компилятор Svelte анализирует то, что вы написали, и генерирует оптимизированный выходной код JavaScript.
Чтобы изучить, как Svelte это делает, давайте начнем с самого простого примера и постепенно будем добавлять к нему код. В процессе вы увидите, что Svelte постепенно добавляет к итоговому коду изменения, основанные на ваших действиях.
Первый пример:
<h1>Hello World</h1>
После компиляции мы получим:
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<h1>Hello World</h1>`);
export default function App($$anchor) {
var h1 = root();
$.append($$anchor, h1);
}
Все компоненты в Svelte компилируются в функцию, которая монтирует элементы в DOM.
Эта функция вызывается только один раз при создании компонента — больше ничего не нужно, компонент работает сам.
Если вы посмотрите сюда:
var root = $.from_html(`<h1>Hello World</h1>`);
Здесь у нас есть весь компонент Svelte в виде HTML-строки, переданной в функцию $.from_html
.
Но что она делает?
Хотелось бы показать вам полную реализацию $.from_html
, но фактический код немного сложнее, поэтому позвольте мне обойти его и упростить.
Функция $.from_html
принимает HTML-строку, преобразует ее в элемент <template>
и возвращает функцию, которая возвращает новый клон экземпляра шаблона:
function from_html(html) {
const template = document.createElement('template');
template.innerHTML = html;
return () => template.cloneNode(true).content.firstChild;
}
Складываем все вместе:
// создаём template element
var root = $.from_html(`<h1>Hello World</h1>`);
export default function App($$anchor) {
// создаём новый клон инстанста template
var h1 = root();
// добавляем h1 element в $$anchor element
$.append($$anchor, h1);
}
Добавим реактивное состояния
Теперь, когда мы рассмотрели минимальный набор компонентов Svelte, давайте разберём, как добавление реактивного состояния изменит скомпилированный код:
<script>
let name = $state('World');
</script>
<h1>Hello {name}</h1>
Обратите внимание на изменение на выходе:
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<h1></h1>`);
export default function App($$anchor) {
let name = 'World';
var h1 = root();
h1.textContent = 'Hello World';
$.append($$anchor, h1);
}
Некоторые наблюдения:
То, что вы написали в теге
<script>
, перемещается в начало функцииApp
, при этом удаляется маркер $state runes.В HTML-шаблоне больше нет текстового содержимого в
h1
, вместо этого оно задается с помощьюh1.textContent
.
Здесь произошло следующее: всякий раз, когда Svelte обнаруживает в шаблоне динамическое выражение, то есть элементы, заключенные в {}
, Svelte удаляет их из строки HTML-шаблона. Они не будут определены при клонировании элемента с помощью функции root()
.
Вместо этого динамические значения будут установлены для клонированных элементов только после их создания при настройке компонента в функции App.
Попробуйте добавить статические и динамические атрибуты к элементу и убедитесь, что внутри шаблона действительно определены только статические атрибуты, а динамические атрибуты устанавливаются после создания элемента:
<h1 foo="abc" bar={name}>Hello {name}</h1>
На этом моменте у вас может возникнуть вопрос, почему переменная name
определена в функции App
, хотя она не используется?
Можно было бы прямо сейчас ответить на этот вопрос, но будет нагляднее, если изучить следующее изменение кода, поэтому стоит немного подождать.
Обновление реактивного состояния
Теперь обновим реактивное состояние name
:
<script>
let name = $state('World');
function update() {
name = 'Svelte';
}
</script>
<h1>Hello {name}</h1>
...и посмотрим, как изменился скомпилированный код:
import * as $ from 'svelte/internal/client';
var root = $.from_html(`<h1> </h1>`);
export default function App($$anchor) {
let name = $.state('World');
function update() {
$.set(name, 'Svelte');
}
var h1 = root();
var text = $.child(h1);
$.reset(h1);
$.template_effect(() => $.set_text(text, `Hello ${$.get(name) ?? ''}`));
$.append($$anchor, h1);
}
Некоторые наблюдения:
Появилось дополнительное пространство между тегами
<h1> </h1>
в шаблоне.Подобно предыдущему примеру, то, что вы написали в теге
<script>
, перемещается в начало функцииApp
.Но теперь имя реактивного состояния определяется с помощью
$.state()
.Установка имени реактивного состояния осуществляется с помощью
$.set()
, а чтение — с помощью$.get()
.Существует дополнительная функция
$.template_effect()
, которая используется для установки текстового содержимого текстового узла.
Обновление шаблона
В отличие от предыдущего примера, при каждом изменении имени реактивного состояния мы будем обновлять значение текстового узла внутри <h1>.
Это означает две вещи:
При клонировании шаблона нужно дублировать текстовый узел внутри <h1>.
Н требуется получить ссылку на текстовый узел, чтобы мы могли обновлять его при изменении имени реактивного состояния.
Не знаю, заметили ли вы, но для достижения (1) нам нужно добавить дополнительный пробел между тегами <h1> </h1>
в шаблоне.
Это потому, что при создании шаблона с помощью <h1></h1>
вы получите:
h1
(element)
но при создании шаблона с <h1> </h1>
вы получите:
-
h1
(element)' '
(text node)
С дополнительным пространством это имеет смысл?
h1 ссылается на элемент <h1>
, и чтобы получить ссылку на текстовый узел внутри h1
, нам нужно получить firstChild
, что и делает $.child(h1)
:
var text = $.child(h1);
// is same as
var text = h1.firstChild;
Когда мы создали текстовый узел и имеем ссылку на него, пришло время обновить элемент при изменении имени реактивного состояния. Таким образом, пора поговорить о сигналах и эффектах.
Signals и effects
Мы не будем подробно останавливаться на концепции сигналов, но для более глубокого погружения в тему, можете воспользоваться следующими статьями:
Для дальнейшего понимания разберем несколько ключевых моментов, касающихся сигналов в Svelte, чтобы вы могли понять, как они работают.
Сигналы в Svelte создаются с помощью $.state()
, который компилируется из рун $state
. Сигналы можно рассматривать как объект, который хранит значение реактивного состояния. Для чтения и установки значения необходимо использовать $.get()
и $.set()
.
Сами по себе сигналы бесполезны, если не использовать их с эффектами, например, $.template_effect()
. Функция эффекта принимает функцию обратного вызова и немедленно вызывает ее. Например:
$.template_effect(() => console.log('Hello'));
немедленно вызовет console.log(„Hello“)
.
Очевидно, что само по себе это не будет полезно.
Суперспособность эффекта заключается в том, что он также отслеживает, какие значения сигналов считываются в функции обратного вызова. И всякий раз, когда значение сигнала изменяется, функция обратного вызова вызывается снова.
Итак, вот такой вариант выведет значение имени сигнала один раз, а также каждый раз, когда значение имени сигнала изменится.
$.template_effect(() => console.log($.get(name)));
Вернемся к нашему примеру, когда у нас есть:
$.template_effect(() => $.set_text(text, `Hello ${$.get(name) ?? ''}`));
Эффект будет запускать $.set_text()
всякий раз, когда значение имени сигнала изменяется, чтобы обновить содержимое текстового узла.
Кстати, $.set_text(text, „...“)
— это то же самое, что text.nodeValue = „...“
.
Теперь, если сложить всё вместе, становится ясно: однократный вызов функции App
приводит к созданию нового экземпляра элемента из шаблона и настройке эффекта, который будет автоматически обновлять содержимое текстового узла каждый раз, когда меняется значение сигнала name
.
Теперь мы на один шаг ближе к полному реактивному компоненту Svelte.
Добавление слушателей событий
Теперь добавим слушателей событий:
<script>
let name = $state('world');
function update() {
name = 'Svelte';
}
</script>
<h1 onclick={update}>Hello {name}</h1>
Посмотрите на скомпилированный результат:
import * as $ from 'svelte/internal/client';
function update(_, name) {
$.set(name, 'Svelte');
}
var root = $.from_html(`<h1> </h1>`);
export default function App($$anchor) {
let name = $.state('world');
var h1 = root();
h1.__click = [update, name];
// ...
}
$.delegate(['click']);
Некоторые наблюдения:
Функция обновления перенесена из функции
App
,h1.__click
является массивом,[update, name]
,появилось
$.delegate([„click“])
в конце файла.
Перемещение функции обновления из функции App
Этот шаг является, скорее, оптимизацией, чем смысловым кодом.
Из исходного кода компонента Svelte мы знаем, что функция update
будет добавлена в качестве слушателя события клика к элементу h1
.
Таким образом, вместо повторного объявления update
для каждого экземпляра компонента App
, в скомпилированном выводе эта функция переписывается так, что все ее переменные области действия передаются в качестве аргументов.
Вместо того чтобы создавать update
для каждого компонента, мы выносим одну общую функцию наружу и переиспользуем её.
И посмотрите на скомпилированный результат:
function update() {
// name is referenced from the scope
$.set(name, 'Svelte');
}
// is rewritten as
function update(_, name) {
// name is passed in as the 2nd argument
$.set(name, 'Svelte');
}
Теперь давайте посмотрим, как функция обновления добавляется в качестве слушателя события клика к элементу h1
.
__click и $.delegate
И здесь мы снова сталкиваемся с оптимизацией — на этот раз с делегированием событий, о котором уже шла речь выше.
Суть приёма в том, что вместо того, чтобы вешать обработчик напрямую на каждый элемент, мы регистрируем его один раз — на общем контейнере (в данном случае на корне документа). Для этого в коде достаточно вызвать:
$.delegate(['click']);
Это добавляет делегированного слушателя событий клика в корень документа.
Но как делегированный слушатель событий клика узнает, какой элемент и какого слушателя событий вызвать?
Здесь на помощь приходит свойство __click
.
Можно представить, что делегированный слушатель событий клика реализован примерно так:
document.addEventListener('click', (event) => {
let target = event.target;
while (target !== document.body) {
if (target.__click) {
const [fn, ...args] = target.__click;
fn(event, ...args);
if (event.cancelBubble) {
// if event.stopPropagation() is called,
// stop going up the DOM tree
break;
}
}
// go up the DOM tree
target = target.parentElement;
}
});
Делегированный слушатель событий клика будет проходить по дереву DOM от элемента, по которому был выполнен клик, и если какой-либо из элементов имеет свойство click
, он вызовет функцию, хранящуюся в свойстве click
, с переданными событием и аргументами.
Прелесть делегирования событий в том, что нам не нужно повторно регистрировать слушателя событий при создании нового экземпляра компонента App
и отменять регистрацию при демонтировании и очистке, потому что один и тот же делегированный слушатель событий клика работает с любым количеством элементов и слушателей событий.
Сложив все вместе
Теперь у нас есть полноценный реактивный компонент Svelte, который имеет:
реактивное состояние,
слушателя событий клика
и динамическое выражение в шаблоне.
Давайте пройдемся по скомпилированному коду, чтобы посмотреть, как инициализируется компонент и как при нажатии на элемент h1
обновляется его текстовое содержимое.
Начнем с инициализации компонента App:
-
Код внутри тега
<script>
из компонента Svelte копируется в верхнюю часть функции App.Мы начинаем с инициализации имени реактивного состояния с помощью
$.state(„world“)
.Это создает объект сигнала, а начальное значение устанавливается как
„world“
.
Создаем элементы DOM, клонируя их из шаблона с помощью
$.from_html
иroot()
.Определяем слушателя события клика с помощью
h1.__click = [update, name]
.Получаем ссылку на текстовый узел внутри элемента
h1
с помощью$.child(h1)
.-
Определяем эффект шаблона с помощью
$.template_effect
и устанавливаем содержимое текстового узла равным«Hello world»
.Поскольку мы читаем значение сигнала name внутри эффекта шаблона, этот эффект шаблона теперь отслеживает изменения сигнала name.
Наконец, элемент
h1
добавляется к документу.
Теперь давайте посмотрим, как при нажатии на элемент h1
будет обновляться его текстовое содержимое:
При нажатии на элемент
h1
вызывается делегированный слушатель событияclick
.Делегированный слушатель события
click
находит элемент со свойством__click
и вызывает функцию update.Функция
update
вызывает функцию$.set()
, чтобы обновить значение имени сигнала.-
Поскольку значение имени сигнала изменяется на «Svelte», вызывается эффект шаблона:
Эффект шаблона устанавливает текстовое содержимое текстового узла на
«Hello Svelte»
.Поскольку мы читаем значение имени сигнала внутри эффекта шаблона, эффект шаблона отслеживает следующие изменения имени сигнала.
И вот так текст элемента h1 обновляется до «Hello Svelte» при нажатии.
Заключительная ремарка
Понимание того, как Svelte 5 компилирует ваш код в оптимизированный JavaScript, не только демистифицирует магию фреймворка, но и позволяет глубже оценить его эффективность и минимализм. Мы прошли путь от базовых DOM-операций через шаблоны и делегирование событий до реактивных сигналов и эффектов, увидев, как простой синтаксис Svelte превращается в производительный код без лишнего балласта. Это знание поможет вам писать более осознанные компоненты, отлаживать сложные сценарии и даже вносить вклад в развитие экосистемы Svelte.
НЛО прилетело и оставило здесь промокод для читателей нашего блога:
-15% на заказ любого VDS (кроме тарифа Прогрев) — HABRFIRSTVDS.