[Перевод] Настоящее реактивное программирование в Svelte 3.0

Заголовок статьи может показаться немного кричащим, впрочем как и сам фреймворк Svelte и те идеи, что стоят за ним. Если вы ещё не знаете ничего про Svelte, пристегнитесь, сейчас мы рванём навстречу революции.

Учтите, что это не урок по началу работы со Svelte. Уже существует прекрасное пошаговое интерактивное руководство от команды Svelte, которое погрузит вас в мир реактивного программирования.

kqpxiqtrxj6jssr0bhk5dpeq2ho.jpeg

Оговорка: я не рок-звезда в программировании и не знаю всего на свете. Я просто с энтузиазмом отношусь к новым веяниям, которые случаются ежедневно, и мне нравится, когда я могу, говорить о них — так появилась и эта статья. Отнеситесь к ней критически и обязательно дайте знать, если я написал что-нибудь нелепое.

Хорошо, теперь давайте углубимся в тему!


Но сначала, React

Прежде чем рассказывать, почему я считаю, что Svelte всех порвёт, давайте взглянем на этот недавний твит от человека по имени Dan, и попытаемся понять, что он имел ввиду:

xoy2_0ue8f0170yqgnqsyv-yun8.png

Хм, а почему он тогда называется React?

Ещё одна оговорка: эта статья никоим образом не предназначена для критики React. Я решил использовать его в качестве примера, потому что большинство людей, которые читают эту статью, имели дело с React в тот или иной момент своей жизни. Просто сейчас это лучший пример для противопоставления Svelte.

Что же имел в виду Dan, и как это повлияло на то, как мы сейчас пишем код? Чтобы ответить на эти вопросы, позвольте мне упрощённо рассказать о том, как React работает под капотом.

Когда происходит отрисовка React-приложения, копия DOM помещается в структуру, называемую Virtual DOM. Этот виртуальный DOM выступает посредником между вашим React-кодом и тем, что браузер отображает в DOM.

Затем, при изменении данных (например, после вызова this.setState или useState), React проделывает небольшую работу, чтобы определить, какие части приложения нужно перерисовать.

Он сравнивает виртуальный DOM с реальным, чтобы определить, что изменилось после обновления данных. Затем перерисовывает только те части DOM, которые имеют отличия в виртуальном DOM, устраняя необходимость каждый раз при любом изменении, перерисовывать весь DOM целиком.

Всё происходит очень быстро, потому что обновлять виртуальной DOM намного дешевле, чем реальный, и React обновляет только необходимые кусочки реального DOM. Эта статья намного лучше объясняет этот процесс.

Вероятно, вы заметили одну особенность. Если вы не сообщите React, что данные изменились (вызвав this.setState или эквивалентный хук), виртуальный DOM не изменится, и от React не последует никакой реакции (та-дам!).

Вот что имел в виду Dan, когда сказал, что React не полностью реактивен. React полагается на то, что вы самостоятельно отслеживаете данные своего приложения и сообщаете об их изменениях. Это прибавляет вам работы.


Хорошо, теперь про Svelte

Svelte — это совершенно новый способ создавать веб-приложения невероятно быстрым, эффективным и по-настоящему реактивным способом. И всё это без использования виртуального DOM и с меньшим количеством строк кода, чем понадобилось бы в другом фреймворке или библиотеке.

Это всё, конечно, звучит прекрасно, но чем он отличается от множества других JavaScript библиотек и фреймворков? — спросите вы. Я расскажу.


1. Настоящая реактивность

Svelte — это не библиотека. Svelte — это не фреймворк. Скорее, Svelte — это компилятор, который получает ваш код и выдает нативный JavaScript, который напрямую взаимодействует с DOM без необходимости в каком-либо посреднике.

Погоди-погоди, что? Компилятор? Да — компилятор. И это чертовски хорошая идея, я не могу понять, почему она не была столь очевидной до сих пор. Дальше расскажу, почему я считаю её очень крутой.

Вот цитата из доклада Rich Harris на конференции YGLF 2019:


Svelte 3.0 выносит реактивность из API компонента в сам язык.

Что это означает? Что ж, мы уже знаем, что React (и большинство других фреймворков) требует использовать API, чтобы сообщить ему, об изменении данных (вызов this.setState или useState) и записать их виртуальный DOM.

Необходимость вызова this.setState в React (и иных UI фреймворках и библиотеках) означает, что реактивность вашего приложения теперь привязана к определенному API, без которого оно вообще ничего не будет знать об изменениях в данных.

Svelte использует другой подход.

Его способ исполнения кода был вдохновлён тем, как это сделано в Observable. Вместо обычного запуска кода сверху вниз, он исполняется в топологическом порядке. Посмотрим на фрагмент кода ниже и разберём, что значит — запустить его в топологическом порядке.

1. (() => {
2.   let square => number => number * number;
3.
4.   let secondNumber = square(firstNumber);
5.   let firstNumber = 42;
6.
7.   console.log(secondNumber);
8. })();

При исполнении этого кода сверху вниз, будет показана ошибка в строке №4, потому что для secondNumber используется значение firstNumber, которое на этот момент ещё не было инициализировано.

Если же запустить этот код в топологическом порядке, не будет никаких ошибок. Как так? Компилятор не будет запускать этот код сверху вниз, а рассмотрит все переменные в коде и сгенерирует граф зависимостей (то есть, кто от кого зависит изначально).

Предельно упрощённый путь компилятора, при топологическом прохождении нашего примера, выглядит так:

1. Зависит ли новая переменная 'square' от любой другой переменной?
     - Нет, инициализирую её

2. Зависит ли новая переменная 'secondNumber' от любой другой переменной?
     - Она зависит от 'square' и 'firstNumber'. Я уже инициализировал 'square', но ещё не инициализировал 'firstNumber', что я сейчас и сделаю.

3. Прекрасно, я инициализировал 'firstNumber'. Теперь я могу инициализировать 'secondNumber' используя 'square' и 'firstNumber'
     - Есть ли у меня все переменные, требуемые для запуска выражения 'console.log'?
     - Да, запускаю его.

На первый взгляд кажется, что код отрабатывает сверху вниз, но если присмотреться, можно заметить, что это не так.

Когда компилятор доходит до строки №4, он обнаруживает, что у него нет firstNumber, поэтому он приостанавливает дальнейшее выполнение и просматривает весь код, в поисках инициализации этой переменной. Что ж, именно это происходит в строке №5, поэтому сначала исполняется строка №5, а затем исполнение кода вернётся к строке №4 и пойдёт далее.


Если кратко: при условии, что выражение A зависит от выражения B, выражение B будет выполняться первым независимо от порядка объявления этих выражений.

Итак, как это соотносится с тем, как Svelte реализует свою настоящую реактивность? Вы можете обозначить любое выражение JavaScript специальной меткой. Это выглядит следующим образом: $: foo = bar. То есть, всё, что нужно сделать, это добавить метку с именем $ перед выражением foo = bar (в strict mode этого сделать не получится, если foo не была определена ранее).

Так что, когда Svelte видит любое выражение с префиксом $:, он знает, что переменная слева получает свое значение из переменной справа. Теперь у нас есть способ привязки значения одной переменной к значению другой.

Это и есть реактивность! Прямо сейчас мы использовали часть стандартного API самого JavaScript для достижения настоящей реактивности без необходимости возиться со сторонними API типа this.setState.

Вот как это выглядит на практике:

1. // ванильный js
2. let foo = 10;
3. let bar = foo + 10; // bar теперь равен 20
4. foo = bar // bar всё ещё равен 20 (нет реактивности)
5. bar = foo + 10 // теперь bar равен 30

6. // svelte js
7. let foo = 10;
8. $: bar = foo + 10; // bar равен 20
9. foo = 15 // тут bar станет равным 25, потому что зависит от значения foo

Обратите внимание, что в этом примере нам не нужно было заново пересчитывать bar с новым значением foo, ни напрямую, ни повторным исполнением bar = foo + 10, ни путём вызова метода API, вроде this.setState ({bar = foo + 10}). Это делается автоматически.

То есть, когда вы изменяете foo на 15, bar автоматически обновится на 25, и вам не нужно вызывать никакое API, чтобы сделать это. Svelte уже обо всём знает.

Часть скомпилированного Javascript кода, приведенного выше примера, выглядит примерно так:

1. //... 
2. function instance($$self, $$props, $$invalidate) {
3.   let foo = 10; // bar равен 20

4.   $$invalidate('foo', foo = 15) // тут bar станет равным 25, потому что зависит от значения foo

5.   let bar;

6.   $$self.$$.update = ($$dirty = { foo: 1 }) => {
7.     if ($$dirty.foo) { $$invalidate('bar', bar = foo + 10); }
8.   };

9.   return { bar };
10. }
11. //...

Не торопитесь читать дальше, изучите этот фрагмент кода. Не спеша.

Заметили, что обновление значения foo происходит до того, как bar будет объявлен? Это потому, что компилятор анализирует Svelte-код в топологическом порядке, а не построчно сверху вниз.

Svelte самостоятельно реагирует на изменения данных. Вам не нужно отслеживать, что именно меняется и когда — это происходит само собой.

Примечание: В строке №4, значение bar не будет обновлено, пока следующая итерация цикла EventLoop не подчистит все хвосты.

Таким образом, нам больше не нужно заботиться о ручном обновлении состояния при изменении данных. Вы можете весь день думать над логикой приложения, пока Svelte занимается согласованием UI с актуальным стейтом.


2. Краткость

Помните, ранее я писал, что Svelte позволяет делать больше, написав при этом меньше строк кода? Я покажу простой компонент в React и его эквивалент в Svelte, и вы сами убедитесь:

uecmvcjvkkynahx4u_smbpn1hzc.png

17 строк кода против 29

Эти два приложения полностью идентичны по функциональности, но вы можете видеть, сколько кода нам потребовалось написать в React.js —  и даже не просите меня делать это ещё и в Angular .

qpuievshnyftbdtzgotusv5spti.png

Я старейший из разработчиков

Помимо того, что код Svelte более приятен для глаз, также в нём гораздо проще разобраться, поскольку в нем меньше конструкций. Например, нам не нужен обработчик событий для обновления значения текстового поля —  достаточно просто сделать привязку.

Представьте, что вы только начали изучать веб-разработку. Какой код был бы для вас менее понятен? Тот, что слева, или тот, который справа?

Хотя это может показаться очевидным, но быстро становится понятно, насколько полезнее писать меньше строк кода, когда вы начинаете создавать большие и более сложные приложения. Я лично потратил часы, пытаясь понять, как работает большой React-компонент, который написал мой коллега.

Я искренне верю, что такой упрощенный API в Svelte позволит намного быстрее читать и понимать код, улучшая нашу общую продуктивность.


3. Производительность

Хорошо, мы увидели, что Svelte по-настоящему реактивный и позволяет делать больше с меньшими усилиями. Как насчет производительности? И насколько удобны приложения, написанные полностью в Svelte?

Одна из причин, почему React настолько мощный, заключается в том, что он использует виртуальный DOM для возможности перерисовки пользовательского интерфейса приложения небольшими частями, устраняя необходимость перерисовывать весь DOM каждый раз, когда что-то меняется (что действительно очень дорого).

Однако, недостатком этого подхода является то, что в случае изменения данных компонента React будет повторно перерисовывать не только сам компонент, но и все его дочерние элементы независимо от того, менялись они или нет. Вот почему в React существуют такие методы API, как shouldComponentUpdate, useMemo, React.PureComponent и т.д.

Это проблема всегда будет иметь место, при использовании виртуального DOM для отрисовки пользовательского интерфейса при изменении состояния приложения.

Svelte не использует виртуальный DOM, но как же тогда он решает проблему перерисовки DOM согласно новым данным состояния? Что ж, позвольте мне снова привести цитату Rich Harris из его замечательного выступления на YGLF:


Фреймворки — это не инструменты организации вашего кода. Это инструменты для организации вашего разума.

Эта мысль привела к идее, что фреймворк может быть чем-то, что работает ещё на этапе сборки приложения, устраняя таким образом необходимость для вашего кода иметь посредника в рантайме. Благодаря этой идее, Svelte стал компилятором, а не фреймворком.

Эта простая идея, также объясняет почему Svelte очень быстрый. Svelte компилирует ваш код в эффективный низкоуровневый Javascript, который напрямую взаимодействует с DOM. Это всё прекрасно, но как Svelte решает проблему перерендеринга всего DOM целиком при изменении данных?

Разница заключается в том, каким образом традиционные фреймворки (например, React) и Svelte узнают, что в состоянии что-то изменилось. Мы уже ранее обсудили, что в React необходимо вызвать метод API, чтобы сообщить ему, когда данные изменяются. В случае Svelte достаточно просто использовать оператор присваивания =.

Если значение переменой состояния — скажем, foo — обновляется при помощи оператора =, Svelte, как мы уже знаем, обновит и все другие переменные, которые зависят от foo. Это позволяет Svelte перерисовывать только те части DOM, которые так или иначе получают своё значение из foo.

Я не буду описывать фактическую реализацию этого процесса, потому что эта статья и без того уже достаточно объемная. Но вы можете посмотреть, как сам Rich Harris рассказывает об этом.


В заключение

Svelte 3.0 — одна из лучших вещей, которая появилась в области веб-разработки за последнее время. Некоторые могут назвать это преувеличением, но я не соглашусь. Концепция Svelte и ее реализация позволяют нам создавать большие приложения, при этом отправляя меньше Javascript-кода в браузер пользователя.

Также он позволяет приложениям быть более производительными, более легковесными и при этом получается код, который легче читать. Так сможет ли Svelte заменить в ближайшее время React, Angular или любой другой традиционный UI фреймворк?

Пока я отвечу — нет. Svelte слишком молод по сравнению с ними, поэтому ему нужно время, чтобы вырасти, повзрослеть и разобраться в некоторых причудах, о которых мы пока даже не подозреваем.

Подобно тому, как React своим появлением изменил разработку веб-приложений, у Svelte тоже есть потенциал изменить наше представление о фреймворках и возможно, весь процесс мышления при создании приложений.

Счастливого кодинга!

© Habrahabr.ru