[Перевод] Компилируем 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>

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

  • 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 -->

Svelte REPL

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>

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 для обновления текстового узла.

  • 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>

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 теперь возвращает 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>

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

  • Возвращает фрагмент, который представляет собой инструкцию по созданию фрагмента DOM для компонента.

2. instance

  • Большая часть кода, написанного в теге <script>, находится здесь.

  • Возвращает список переменных экземпляра, на которые есть ссылка в шаблоне.

  • $$invalidate вставляется после каждого присваивания и изменения переменной экземпляра

3. class App extends SvelteComponent

  • Инициализирует компонент с помощью create_fragment и instance

  • Устанавливает внутренние части компонента

  • Предоставляет API компонента

Svelte стремится создать как можно более компактный JavaScript, например:

  • Разбиение текстового содержимого h1 на отдельные текстовые узлы только тогда, когда часть текста может быть обновлена

  • Не определяет create_fragment или instance, когда это не нужно

  • Генерирует dispose как массив или функцию, в зависимости от количества слушателей событий.

Заключение

Мы рассмотрели базовую структуру кода, которую генерирует компилятор Svelte и это только начало.


Надеюсь данный материал был для вас полезен!

Let’s block ads! (Why?)

Read More

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *