Введение
Давайте вспомним как мы пишем веб-приложения без фреймворков:
Создаем элемент
// создаем элемент 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
. В нем есть такие методы, как:
c()
Сокращенно от 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 и это только начало.
От переводчика:
Присоединяйтесь к русскоязычному сообществу Svelte в Телеграм - @sveltejs. Там вы сможете найти помощь или совет практически по любым вопросам, а также обсудить самые актуальные темы. Если нет времени на чаты, подписывайтесь на канал @sveltejs_public, где публикуются новости и полезные материалы по Svelte.
Надеюсь данный материал был для вас полезен!
inv2004
Добавлю что недавно столкнулся с ощутимой разницей в скорости работы vue и svelte. Список из ~200 элементов, при этом просто проведение мышкой над ними в vue отжирает до 40% cpu на i5, при этом заметный лаг в изменении параметра, завязанного на mouseover над отдельными элементами. При этом в svelte всё работает мгновенно, при этом график cpu максимум на 5-10% прыгает.
chshanovskiy
Есть вероятность, что в обработчике mouseover происходило что-то, что вызывало перерисовку всего списка полностью. Например изменение состояния родительского контейнера. Такая проблема характерна и для vue и для react, однако причина и решение хорошо разобраны в документациях.
inv2004
Если только во внутренностях vue, или в библиотеке компонентов. У меня в коде ничего не менялось сверху. Компоненты просто подсвечивали строку на mouseover. Это их стандартное повередние и в quasar и svelte-elements
justboris
Сравнение интересное, но чем то смахивает на лечение головной боли через гильотину. Точно не было другого варианта решить эту проблему и остаться на Vue?
moodpulse
Лучше сказать, что вариант решить проблему точно был.
Возможно это одна из классических проблем vue/react и подобного.
Без кода это просто голословное «vue» *овно
inv2004
Точно был, например pagination сделать. Однако это был просто список на 300 элементов на библиотеке компонентов. Делать sandbox немного лень, но если кто-то очень сомневается, то конечно лучше перепроверить. Я не считаю, что vue говно, имхо намного удобнее реакта, мой комментарий был просто про скорость на одинаковом функционале.
ilbu
Боюсь что вы все-таки где-то выстрелили себе в ногу и причина была вовсе не во Vue. Svelte, безусловно, быстрее, но в описанном вами кейсе дело явно в чем-то другом :D
moodpulse
Да, кстати, может и быстрее, но не на такой относительно мелочи, на таких кейсах вообще должно быть не заметно
MaZaAa
Всё просто, вы просто не правильно написали код. Проблема не во Vue или React. Проблема в не знании как правильно с ними работать.
Опубликуйте ваш код на codesandbox.io который тормозит на Vue или React'e, я его поправлю, чтобы тормозов не было и покажу вам.
inv2004
если можно я отложу создание минимального примера на какой-то срок (но не совсем) так как за доказательством комментариев можно потерять нить других дел, что совсем делать не хочется