[Перевод] Компилируем Svelte в уме. Часть 1/3
Введение
Давайте вспомним как мы пишем веб-приложения без фреймворков:
Создаем элемент
// создаем элемент h1
const h1 = document.createElement('h1');
h1.textContent = 'Hello World';
// ...и добавляем его в body
document.body.appendChild(h1);
Обновляем элемент
// обновляем текст элемента h1
h1.textContent = 'Bye World';
Удаляем элемент
// наконец, мы удаляем элемент h1
document.body.removeChild(h1);
Добавляем стили к элементу
const h1 = document.createElement('h1');
h1.textContent = 'Hello World';
// добавляем класс к элементу h1
h1.setAttribute('class', 'abc');
// ...и добавляем тег <style> в head
const 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!';
// слушаем событие click
button.addEventListener('click', () => {
console.log('Hi!');
});
document.body.appendChild(button);
На чистом JavaScript нам нужно написать что-то подобное.
Основная цель данной статьи в том чтобы показать как компилятор Svelte преобразует синтаксис Svelte в блоки кода, которые я показал выше.
Синтаксис Svelte
Далее я покажу базовые примеры Svelte синтаксиса.
Если вы хотите узнать подробнее, рекомендую попробовать интерактивный Svelte туториал.
Итак, вот простейший компонент Svelte:
<h1>Hello World</h1>
Для добавления стилей, нужно добавить тег <style>
:
<style>
h1 {
color: rebeccapurple;
}
</style>
<h1>Hello World</h1>
На этом этапе написание Svelte компонента ощущается аналогично тому как мы пишем обычный HTML, потому что синтаксис Svelte является надмножеством HTML синтаксиса.
Давайте посмотрим, как мы добавляем данные в наш компонент:
<script>
let name = 'World';
</script>
<h1>Hello {name}</h1>
Мы помещаем переменную JavaScript в фигурные скобки.
Чтобы добавить обработчик клика, мы используем директиву on:
<script>
let count = 0;
function onClickButton(event) {
console.log(count);
}
</script>
<button on:click={onClickButton}>Clicked {count}</button>
Для изменения данных мы используем операторы присваивания:
<script>
let count = 0;
function onClickButton(event) {
count += 1;
}
</script>
<button on:click={onClickButton}>Clicked {count}</button>
Давайте посмотрим как синтаксис Svelte компилируется в JavaScript, который мы видели ранее.
Компилируем Svelte в уме
Компилятор Svelte анализирует код, который вы пишете и генерирует оптимизированный JavaScript.
Чтобы понять, как Svelte компилирует код, давайте начнем с наименьшего возможного примера и постепенно будем усложнять его. В процессе вы увидите, что именно Svelte добавляет к конечному коду на основе наших изменений.
Первый пример на который мы посмотрим:
<h1>Hello World</h1>
Код который получится на выходе:
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
-
class App extends SvelteComponent
create_fragment
Компоненты Svelte – это строительные блоки приложения Svelte. Каждый компонент Svelte фокусируется на построении своей части или фрагменте финального DOM дерева.
Функция create_fragment
дает компоненту Svelte руководство по созданию фрагмента DOM дерева.
Посмотрите на возвращаемый объект функции create_fragment
. В нем есть такие методы, как:
Сокращенно от create. Содержит инструкции по созданию всех элементов во фрагменте.
В этом примере метод содержит инструкции по созданию элемента h1
:
h1 = element('h1');
h1.textContent = 'Hello World';
-
m(target, anchor)
Сокращенно от mount. Содержит инструкции для вставки элементов в указанную цель.
В этом примере метод содержит инструкции по вставке элемента h1
в target
:
insert(target, h1, anchor);
// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.ts
export function insert(target, node, anchor) {
target.insertBefore(node, anchor || null);
}
-
d(detaching)
Сокращенно от destroy. Содержит инструкции по удалению элементов из указанной цели.
В этом примере мы удаляем элемент h1
из DOM дерева:
detach(h1);
// http://github.com/sveltejs/svelte/tree/master/src/runtime/internal/dom.ts
function detach(node) {
node.parentNode.removeChild(node);
}
Имена методов сокращены для лучшей минификации. Посмотрите, что не может быть минифицированно.
export default class App extends SvelteComponent
Каждый компонент – это класс, который вы можете импортировать и создать экземпляр через этот API.
В конструкторе мы инициализируем компонент с информацией из которой он состоит, например create_fragment. Svelte будет передавать только необходимую информацию и удалять ее всякий раз, когда в этом нет необходимости.
Попробуйте удалить тег <h1>
и посмотрите, что произойдет с выводом:
<!-- empty -->
class App extends SvelteComponent {
constructor(options) {
super();
init(this, options, null, null, safe_not_equal, {});
}
}
Svelte передаст null
вместо create_fragment
!
Функция init – это то место, где Svelte настраивает большинство внутренних частей, таких как:
-
входные параметры компонента,
ctx
и контекст -
события жизненного цикла
-
механизм обновления компонента
в самом конце Svelte вызывает create_fragment
для создания и монтирования элементов в DOM.
Если вы заметили, все внутренние состояния и методы привязаны к this.$$
.
Поэтому, если вы обращаетесь к свойству $$
компонента, вы подключаетесь к внутренним частям компонента. Вы предупреждены!
Добавление данных
Теперь когда мы рассмотрели минимальный Svelte компонент, давайте посмотрим как добавление данных изменит скомпилированный код:
<script>
let name = 'World';
</script>
<h1>Hello {name}</h1>
Обратите внимание на изменение вывода:
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>
… и посмотрим на изменение скомпилированного кода:
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
для обновления текстового узла.
-
p(ctx, dirty)
Сокращенно от 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>
И обратите внимание на разницу:
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
теперь возвращает 2 переменных вместо одной -
добавлен вызов
listen
в функцииmount
иdispose
в функцииdestroy
Как я упоминал ранее, функция instance
возвращает переменные, на которые есть ссылка в шаблоне и которые изменены или переназначены.
Поскольку мы только что сослались на функцию update
в шаблоне, теперь она возвращается в функции instance
как часть ctx
.
Svelte пытается сгенерировать как можно более компактный вывод JavaScript, не возвращая лишнюю переменную, если в этом нет необходимости.
listen и dispose
Каждый раз, когда вы добавляете слушатель событий в шаблоне, Svelte добавляет соответствующий слушатель и удаляет его, когда фрагмент удаляется из DOM.
Попробуем добавить больше слушателей событий,
<h1
on:click={update}
on:mousedown={update}
on:touchstart={update}>
Hello {name}!
</h1>
и посмотрим на вывод компилятора:
// ...
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
-
Возвращает фрагмент, который представляет собой инструкцию по созданию фрагмента DOM для компонента.
2. instance
-
Большая часть кода, написанного в теге
<script>
, находится здесь. -
Возвращает список переменных экземпляра, на которые есть ссылка в шаблоне.
-
$$invalidate
вставляется после каждого присваивания и изменения переменной экземпляра
3. class App extends SvelteComponent
-
Инициализирует компонент с помощью
create_fragment
иinstance
-
Устанавливает внутренние части компонента
-
Предоставляет API компонента
Svelte стремится создать как можно более компактный JavaScript, например:
-
Разбиение текстового содержимого
h1
на отдельные текстовые узлы только тогда, когда часть текста может быть обновлена -
Не определяет
create_fragment
илиinstance
, когда это не нужно -
Генерирует
dispose
как массив или функцию, в зависимости от количества слушателей событий. -
…
Заключение
Мы рассмотрели базовую структуру кода, которую генерирует компилятор Svelte и это только начало.
Надеюсь данный материал был для вас полезен!