Введение
Давайте вспомним как мы пишем веб-приложения без
фреймворков:
Создаем элемент
// создаем элемент h1const h1 = document.createElement('h1');h1.textContent = 'Hello World';// ...и добавляем его в bodydocument.body.appendChild(h1);
Обновляем элемент
// обновляем текст элемента h1h1.textContent = 'Bye World';
Удаляем элемент
// наконец, мы удаляем элемент h1document.body.removeChild(h1);
Добавляем стили к элементу
const h1 = document.createElement('h1');h1.textContent = 'Hello World';// добавляем класс к элементу h1h1.setAttribute('class', 'abc');// ...и добавляем тег <style> в headconst style = document.createElement('style');style.textContent = '.abc { color: blue; }';document.head.appendChild(style);document.body.appendChild(h1);
Слушаем события click на элементе
const button = document.createElement('button');button.textContent = 'Click Me!';// слушаем событие clickbutton.addEventListener('click', () => { console.log('Hi!');});document.body.appendChild(button);
На чистом JavaScript нам нужно написать что-то подобное.
Основная цель данной статьи в том чтобы показать как компилятор
Svelte преобразует синтаксис Svelte в блоки кода, которые я показал
выше.
Синтаксис Svelte
Далее я покажу базовые примеры Svelte синтаксиса.
Если вы хотите узнать подробнее, рекомендую попробовать интерактивный Svelte
туториал.
Итак, вот простейший компонент Svelte:
<h1>Hello World</h1>
Svelte
REPL
Для добавления стилей, нужно добавить тег
<style>
:
<style> h1 { color: rebeccapurple; }</style><h1>Hello World</h1>
Svelte
REPL
На этом этапе написание Svelte компонента ощущается аналогично
тому как мы пишем обычный HTML, потому что синтаксис Svelte
является надмножеством HTML синтаксиса.
Давайте посмотрим, как мы добавляем данные в наш компонент:
<script> let name = 'World';</script><h1>Hello {name}</h1>
Svelte
REPL
Мы помещаем переменную JavaScript в фигурные скобки.
Чтобы добавить обработчик клика, мы используем директиву
on:
<script> let count = 0; function onClickButton(event) { console.log(count); }</script><button on:click={onClickButton}>Clicked {count}</button>
Svelte
REPL
Для изменения данных мы используем операторы присваивания:
<script> let count = 0; function onClickButton(event) { count += 1; }</script><button on:click={onClickButton}>Clicked {count}</button>
Svelte
REPL
Давайте посмотрим как синтаксис Svelte компилируется в
JavaScript, который мы видели ранее.
Компилируем Svelte в уме
Компилятор Svelte анализирует код, который вы пишете и
генерирует оптимизированный JavaScript.
Чтобы понять, как Svelte компилирует код, давайте начнем с
наименьшего возможного примера и постепенно будем усложнять его. В
процессе вы увидите, что именно Svelte добавляет к конечному коду
на основе наших изменений.
Первый пример на который мы посмотрим:
<h1>Hello World</h1>
Svelte
REPL
Код который получится на выходе:
function create_fragment(ctx) { let h1; return { c() { h1 = element('h1'); h1.textContent = 'Hello world'; }, m(target, anchor) { insert(target, h1, anchor); }, d(detaching) { if (detaching) detach(h1); }, };}export default class App extends SvelteComponent { constructor(options) { super(); init(this, options, null, create_fragment, safe_not_equal, {}); }}
Мы можем разделить данный код на 2 части:
create_fragment
Компоненты Svelte - это строительные блоки приложения Svelte.
Каждый компонент Svelte фокусируется на построении своей части или
фрагменте финального DOM дерева.
Функция create_fragment
дает компоненту Svelte
руководство по созданию фрагмента DOM дерева.
Посмотрите на возвращаемый объект функции
create_fragment
. В нем есть такие методы, как:
Сокращенно от create. Содержит инструкции по созданию всех
элементов во фрагменте.
В этом примере метод содержит инструкции по созданию элемента
h1
:
h1 = element('h1');h1.textContent = 'Hello World';
Сокращенно от mount. Содержит инструкции для вставки элементов в
указанную цель.
В этом примере метод содержит инструкции по вставке элемента
h1
в target
:
insert(target, h1, anchor);// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.tsexport function insert(target, node, anchor) { target.insertBefore(node, anchor || null);}
Сокращенно от destroy. Содержит инструкции по удалению элементов
из указанной цели.
В этом примере мы удаляем элемент h1
из DOM
дерева:
detach(h1);// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.tsfunction detach(node) { node.parentNode.removeChild(node);}
Имена методов сокращены для лучшей минификации. Посмотрите, что
не может быть
минифицированно.
export default class App extends SvelteComponent
Каждый компонент - это класс, который вы можете импортировать и
создать экземпляр через этот API.
В конструкторе мы инициализируем компонент с информацией из
которой он состоит, например create_fragment. Svelte будет
передавать только необходимую информацию и удалять ее всякий раз,
когда в этом нет необходимости.
Попробуйте удалить тег <h1>
и посмотрите, что
произойдет с выводом:
<!-- empty -->
Svelte
REPL
class App extends SvelteComponent { constructor(options) { super(); init(this, options, null, null, safe_not_equal, {}); }}
Svelte передаст null
вместо
create_fragment
!
Функция init - это то место, где Svelte настраивает большинство
внутренних частей, таких как:
в самом конце Svelte вызывает create_fragment
для
создания и монтирования элементов в DOM.
Если вы заметили, все внутренние состояния и методы привязаны к
this.$$
.
Поэтому, если вы обращаетесь к свойству $$
компонента, вы подключаетесь к внутренним частям компонента. Вы
предупреждены!
Добавление данных
Теперь когда мы рассмотрели минимальный Svelte компонент,
давайте посмотрим как добавление данных изменит скомпилированный
код:
<script>let name = 'World';</script><h1>Hello {name}</h1>
Svelte
REPL
Обратите внимание на изменение вывода:
function create_fragment(ctx) { // ... return { c() { h1 = element('h1'); h1.textContent = `Hello ${name}`;}, // ... };}let name = 'World';class App extends SvelteComponent { // ...}
Некоторые наблюдения:
-
то, что мы написали в теге <script>
,
перемещается на верхний уровень кода
-
текстовое содержимое элемента h1
теперь является
шаблонной строкой
Прямо сейчас под капотом происходит много интересных вещей, но
давайте немного подождем, потому что это лучше всего объясняется
при сравнении со следующим изменением кода.
Обновление данных
Давайте добавим функцию для обновления имени:
<script>let name = 'World';function update() {name = 'Svelte';}</script><h1>Hello {name}</h1>
Svelte
REPL
и посмотрим на изменение скомпилированного кода:
function create_fragment(ctx) { return { c() { h1 = element('h1'); t0 = text('Hello '); t1 = text(/*name*/ ctx[0]); }, m(target, anchor) { insert(target, h1, anchor); append(h1, t0); append(h1, t1); }, p(ctx, [dirty]) { if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]); }, d(detaching) { if (detaching) detach(h1); }, };}function instance($$self, $$props, $$invalidate) { let name = 'World'; function update() { $$invalidate(0, (name = 'Svelte')); } return [name];}export default class App extends SvelteComponent { constructor(options) { super(); init(this, options, instance, create_fragment, safe_not_equal, {}); }}
Некоторые наблюдения:
-
текстовое содержимое элемента <h1>
теперь
разбито на 2 текстовых узла, созданных функцией text
(...)
-
объект возвращаемый функцией create_fragment
получил новый метод p(ctx, dirty)
-
появилась новая функция instance
-
то что мы написали в теге script
было перенесено в
функцию instance
-
имя переменной, которое использовалось в
create_fragment
, теперь заменено на
ctx[0]
Почему произошли такие изменения?
Компилятор Svelte отслеживает все переменные, объявленные в теге
<script>
.
Он отслеживает следующие факторы переменной:
-
может быть изменена? например: count++
-
может быть переназначена? например: name =
'Svelte'
-
на переменную ссылаются в шаблоне? например
<h1>Hello {name}</h1>
-
доступна для записи? например const i = 1;
или
let i = 1;
-
... и многое другое
Когда компилятор Svelte понимает, что имя переменной можно
переназначить (из-за name = 'Svelte';
при обновлении),
он разбивает текстовое содержимое h1
на части, чтобы
он мог динамически обновлять часть текста.
И в самом деле, вы можете видеть, что есть новый метод
p
для обновления текстового узла.
Сокращенно от u_p_date
p(ctx, dirty)
содержит инструкции по обновлению
элементов в зависимости от того, что изменилось в состоянии
(dirty
) и состоянии (ctx
) компонента.
Функция instance
Компилятор понимает, что имя переменной не может использоваться
в разных экземплярах компонента App
.
Вот почему он перемещает объявление имени переменной в функцию с
именем instance
.
В предыдущем примере, независимо от того, сколько экземпляров
компонента App
, значение имени переменной одинаково и
не изменяется во всех экземплярах:
<App /><App /><App /><!-- выведет --><h1>Hello world</h1><h1>Hello world</h1><h1>Hello world</h1>
Но в этом примере переменную name
можно изменить в
пределах 1 экземпляра компонента, поэтому объявление этой
переменной теперь перемещено в функцию instance
:
<App /><App /><App /><!-- может быть --><h1>Hello world</h1><h1>Hello Svelte</h1><h1>Hello world</h1><!-- в зависимости от внутреннего состояния компонента -->
instance($$self, $$props, $$invalidate)
Функция instance возвращает список переменных компонента:
В Svelte мы называем этот список переменных
ctx
.
В функции init
Svelte вызывает функцию
instance
для создания ctx
и использует
его при создания фрагмента для компонента:
// концептуально,const ctx = instance(/*...*/);const fragment = create_fragment(ctx);// создаем фрагментfragment.c();// монтируем фрагмент в DOM деревоfragment.m(target);
Теперь вместо доступа к переменной name
вне
компонента мы ссылаемся на name
, переданную через
ctx
:
t1 = text(/* name */ ctx[0]);
Причина, по которой ctx
является массивом, а не
коллекцией Map
или объектом, связана с оптимизацией.
Вы можете увидеть обсуждение этого здесь.
$$invalidate
Секрет системы реактивности в Svelte - кроется в функции
$$invalidate
.
Для каждой переменной, которая была
будет вставлена функция $$invalidate
сразу после
присвоения или изменения:
name = 'Svelte';count++;foo.a = 1;// скомпилируется в примерно такой кодname = 'Svelte';$$invalidate(/* name */, name);count++;$$invalidate(/* count */, count);foo.a = 1;$$invalidate(/* foo */, foo);
Функция $$invalidate
отмечает переменную как
грязную и планирует обновление для компонента:
// концептуально...const ctx = instance(/*...*/);const fragment = create_fragment(ctx);// чтобы отслеживать, какая переменная измениласьconst dirty = new Set();const $$invalidate = (variable, newValue) => { // обновляем ctx ctx[variable] = newValue; // помечаем переменную как грязную dirty.add(variable); // планируем обновление для компонента scheduleUpdate(component);};// вызывается, когда запланировано обновлениеfunction flushUpdate() { // обновить фрагмент fragment.p(ctx, dirty); // очистить список помеченных переменных dirty.clear();}
Добавляем слушатели событий
Теперь добавим слушателя событий
<script>let name = 'world';function update() {name = 'Svelte';}</script><h1 on:click={update}>Hello {name}</h1>
Svelte
REPL
И обратите внимание на разницу:
function create_fragment(ctx) { // ... return { c() { h1 = element('h1'); t0 = text('Hello '); t1 = text(/*name*/ ctx[0]); }, m(target, anchor) { insert(target, h1, anchor); append(h1, t0); append(h1, t1); dispose = listen(h1, 'click', /*update*/ ctx[1]);}, p(ctx, [dirty]) { if (dirty & /*name*/ 1) set_data(t1, /*name*/ ctx[0]); }, d(detaching) { if (detaching) detach(h1); dispose();}, };}function instance($$self, $$props, $$invalidate) { let name = 'world'; function update() { $$invalidate(0, (name = 'Svelte')); } return [name, update];}// ...
Некоторые наблюдения:
Как я упоминал ранее, функция instance
возвращает
переменные, на которые есть ссылка в шаблоне и которые изменены или
переназначены.
Поскольку мы только что сослались на функцию update
в шаблоне, теперь она возвращается в функции instance
как часть ctx
.
Svelte пытается сгенерировать как можно более компактный вывод
JavaScript, не возвращая лишнюю переменную, если в этом нет
необходимости.
listen и dispose
Каждый раз, когда вы добавляете слушатель событий в шаблоне,
Svelte добавляет соответствующий слушатель и удаляет его, когда
фрагмент удаляется из DOM.
Попробуем добавить больше слушателей событий,
<h1on:click={update}on:mousedown={update}on:touchstart={update}> Hello {name}!</h1>
Svelte
REPL
и посмотрим на вывод компилятора:
// ...dispose = [ listen(h1, 'click', /*update*/ ctx[1]), listen(h1, 'mousedown', /*update*/ ctx[1]), listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true }),];// ...run_all(dispose);
Вместо того чтобы объявлять и создавать новую переменную для
удаления каждого слушателя, Svelte присваивает их в массив:
// вместо вот такогоdispose1 = listen(h1, 'click', /*update*/ ctx[1]);dispose2 = listen(h1, 'mousedown', /*update*/ ctx[1]);dispose2 = listen(h1, 'touchstart', /*update*/ ctx[1], { passive: true });// ...dispose1();dispose2();dispose3();
Минификация может сжать имя переменной, но скобки убрать
нельзя.
Опять же, это еще один отличный пример того, как Svelte пытается
сгенерировать компактный вывод JavaScript. Svelte не создает массив
dispose
, если имеется только один слушатель
событий.
Итого
Синтаксис Svelte - это надмножество HTML.
Когда вы пишете компонент Svelte, компилятор анализирует ваш код
и генерирует оптимизированный JavaScript код.
Который можно разделить на 3 сегмента:
1. create_fragment
2. instance
-
Большая часть кода, написанного в теге
<script>
, находится здесь.
-
Возвращает список переменных экземпляра, на которые есть ссылка
в шаблоне.
-
$$invalidate
вставляется после каждого присваивания
и изменения переменной экземпляра
3. class App extends SvelteComponent
-
Инициализирует компонент с помощью create_fragment
и instance
-
Устанавливает внутренние части компонента
-
Предоставляет API компонента
Svelte стремится создать как можно более компактный JavaScript,
например:
-
Разбиение текстового содержимого h1
на отдельные
текстовые узлы только тогда, когда часть текста может быть
обновлена
-
Не определяет create_fragment
или
instance
, когда это не нужно
-
Генерирует dispose
как массив или функцию, в
зависимости от количества слушателей событий.
-
...
Заключение
Мы рассмотрели базовую структуру кода, которую генерирует
компилятор Svelte и это только начало.
Надеюсь данный материал был для вас полезен!