Эта статья — перевод оригинальной статьи "Introducing runes".

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

В 2019 году Svelte 3 превратил JavaScript в реактивный язык. Svelte - это фреймворк для создания веб-интерфейса, который использует компилятор для превращения декларативного кода компонентов в такой...

<script>
	let count = 0;

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	clicks: {count}
</button>

...в жестко оптимизированный JavaScript, который обновляет документ при изменении состояния, например, count. Поскольку компилятор "видит", где ссылаются на count, генерируемый код очень эффективен, а поскольку мы используем такие синтаксисы, как let и =, а не громоздкие API, вы можете писать меньше кода.

Чаще всего мы получаем такие отзывы: "Хотел бы я писать весь свой JavaScript именно так". Когда вы привыкли к тому, что вещи внутри компонентов волшебным образом обновляются, возврат к старому скучному процедурному коду кажется вам переходом от цветного к черно-белому.

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

Прежде чем начать

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

Дата выхода Svelte 5 пока не определена. То, что мы показываем здесь, - это наработки, которые, могут измениться.

Что такое руны?

Руны - это символы, влияющие на работу компилятора Svelte. Если сегодня в Svelte для обозначения конкретных вещей используются let, =, ключевое слово export и метка $:, то руны используют синтаксис функций для достижения того же и даже большего.

Например, чтобы объявить часть реактивного состояния, мы можем использовать руну $state:

<script>
	// let count = 0;
	let count = $state(0);

	function increment() {
		count += 1;
	}
</script>

<button on:click={increment}>
	clicks: {count}
</button>

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

Нет. Реальность такова, что по мере роста сложности приложений выяснение того, какие значения являются реактивными, а какие нет, может стать сложной задачей. К тому же эвристика работает только для объявлений let на верхнем уровне компонента, что может привести к путанице. Если в файлах .svelte код ведет себя одним образом, а в .js - другим, это может затруднить рефакторинг кода, например, если вам нужно превратить что-то в хранилище, чтобы использовать его в нескольких местах.

За пределами компонентов

С помощью рун реактивность выходит за пределы файлов .svelte. Предположим, мы хотим инкапсулировать логику счетчика таким образом, чтобы ее можно было повторно использовать в разных компонентах. Сегодня для этого используется кастомное хранилище в файле .js или .ts:

import { writable } from 'svelte/store';

export function createCounter() {
	const { subscribe, update } = writable(0);

	return {
		subscribe,
		increment: () => update((n) => n + 1)
	};
}

Поскольку в данном случае реализуется контракт с хранилищем - возвращаемое значение имеет метод subscribe, - мы можем ссылаться на значение хранилища, добавляя к его имени префикс $:

<script>
	import { createCounter } from './counter.js';

	const counter = createCounter();
	// let count = 0;

	// function increment() {
	// 	count += 1;
	// }
</script>

// <button on:click={increment}>
// 	clicks: {count}
<button on:click={counter.increment}>
	clicks: {$counter}
</button>

Это работает, но довольно странно! Мы обнаружили, что API хранилища может стать довольно громоздким, когда вы начинаете делать более сложные вещи.

С рунами все гораздо проще:

// import { writable } from 'svelte/store';

export function createCounter() {
	// const { subscribe, update } = writable(0);
	let count = $state(0);

	return {
		// subscribe,
		// increment: () => update((n) => n + 1)
		get count() { return count },
		increment: () => count += 1
   };
}
<script>
	import { createCounter } from './counter.js';

	const counter = createCounter();
</script>

<button on:click={counter.increment}>
	// clicks: {$counter}
	clicks: {counter.count}
</button>

Обратите внимание, что мы используем свойство get в возвращаемом объекте, поэтому counter.count всегда ссылается на текущее значение, а не на значение в момент вызова функции.

Реактивность во время выполнения

Сегодня Svelte использует реактивность во время компиляции. Это означает, что если у вас есть код, использующий метку $: для автоматического перезапуска при изменении зависимостей, то эти зависимости будут определены при компиляции компонента в Svelte:

<script>
	export let width;
	export let height;

	// компилятор знает, что ему следует пересчитать `площадь`.
	// при изменении `ширины` или `высоты`...
	$: area = width * height;

	// ...и что он должен логировать значение `area`.
	// когда оно меняется
	$: console.log(area);
</script>

Это работает хорошо... до того момента пока не перестанет. Предположим, что мы рефакторим приведенный выше код:

const multiplyByHeight = (width) => width * height;
$: area = multiplyByHeight(width);

Поскольку объявление $: area = ... может "видеть" только width, оно не будет пересчитываться при изменении height. В результате код трудно рефакторить, а понимание тонкостей того, когда Svelte решает обновить те или иные значения, может стать довольно сложным после определенного уровня сложности.

В Svelte 5 появились руны $derived и $effect, которые вместо этого определяют зависимости своих выражений при их вычислении:

<script>
	let { width, height } = $props(); // вместо `export let`

	const area = $derived(width * height);

	$effect(() => {
		console.log(area);
	});
</script>

Как и $state, $derived и $effect также могут быть использованы в файлах .js и .ts.

Усиление сигнала

Как и любой другой фреймворк, мы пришли к пониманию того, что Knockout всегда был прав.

Реактивность Svelte 5 обеспечивается сигналами, которые, по сути, являются тем, чем занимался Knockout в 2010 году. Совсем недавно сигналы были популяризированы Solid и приняты множеством других фреймворков.

Однако у нас все немного по-другому. В Svelte 5 сигналы - это детали реализации, а не то, с чем вы взаимодействуете напрямую. Таким образом, мы не имеем тех же ограничений на дизайн API и можем максимально повысить эффективность и эргономичность. Например, мы избегаем проблем с сужением типов, возникающих при обращении к значениям через вызов функции, а при компиляции в режиме рендеринга на стороне сервера мы можем вообще отказаться от сигналов, поскольку на сервере они - не более чем накладные расходы.

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

Впереди более простые времена

Руны - это добавочное свойство, но они делают устаревшими целую кучу существующих концепций:

  • разница между let на верхнем уровне компонента и в остальных местах

  • export let

  • $:, со всеми вытекающими отсюда странностями

  • разное поведение между <script> и <script context="module">

  • API хранилища, часть которого на самом деле довольно сложна

  • префикс хранилища $

  • $$props и $$restProps

  • функции жизненного цикла (такие вещи, как onMount, могут быть просто функциями $effect)

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

Однако это только начало. У нас есть большой список идей для последующих релизов, которые сделают Svelte еще проще и функциональнее.

Попробуй Svelte 5!

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

Но мы не хотели оставлять вас в подвешенном состоянии. Мы создали предварительный сайт с подробным описанием новых возможностей и интерактивной игровой площадкой. Вы также можете посетить канал #svelte-5-runes в Svelte Discord, чтобы узнать больше. Мы будем рады получить ваши отзывы!

Комментарии (21)


  1. olegkusov
    25.09.2023 11:27
    +2

    На первый взгляд Svelte по DX теперь одна из вариаций React


  1. aleksandy
    25.09.2023 11:27
    +1

    контракт с магазином

    Каким, ёжики пушистые, магазином?


    1. qmzik Автор
      25.09.2023 11:27

      Фиксед, благодарю за внимательность


      1. devprodest
        25.09.2023 11:27
        +1

        Там ещё есть ????


        1. qmzik Автор
          25.09.2023 11:27

          Тоже фиксед :)


  1. Pab10
    25.09.2023 11:27
    -1

    Толи еще будет. Скоро и поддержку JSX добавят, ато шаблоны не нативно же! :)


  1. Anstak
    25.09.2023 11:27

    Я использую svelte в 3х больших проектах, реактивность через `$:` часто работает не так как ожидаешь, так что это безусловно шаг вперёд. Да, как-будто бы заимствование из реакта, но свелт намного быстрее в разработке и с поддержкой typescript он стал моим любимым фреймворком. В целом я очень ожидаю это обновление.


    1. ValeryIvanov
      25.09.2023 11:27
      -1

      Ничтожества. Столько лет вам понабилось на то, чтобы прийти к модели свойств JavaFX.

      var width = new SimpleIntegerProperty();
      var height = new SimpleIntegerProperty();
      
      var area = Bindings.multiply(width, height);
      
      area.addListener((observable, oldValue, newValue) -> {
          System.out.println(newValue);
      });
      

      /s


      1. nin-jin
        25.09.2023 11:27

        Что придёт в newValue, когда произведение width и height выйдет за пределы инта?


        1. ValeryIvanov
          25.09.2023 11:27

          В newValue придёт произведение примитивов, которые обёрнуты в SimpleIntegerProperty. Как эти примитивы перемножаются, что будет при переполнении, за это отвечает Java(или, скорее, JVM).


          1. nin-jin
            25.09.2023 11:27

            Ок, спрошу прямее: Что придёт в newValue, когда произведение width и height выкинет ArithmeticException?


            1. ValeryIvanov
              25.09.2023 11:27
              +1

              ArithmeticException при переполнении не будет, это же Java. При любом другом исключении, Binding залогирует исключение и примет значение 0. Это же значение и будет передано в слушатель третьим параметром(newValue).


              1. nin-jin
                25.09.2023 11:27

                С 8 версии есть нормальное сложение.

                Вы считаете 0 при исключениях адекватным поведением?


                1. ValeryIvanov
                  25.09.2023 11:27

                  С 8 версии есть нормальное сложение.
                  Разве? Это включается каким-то флагом или как? По умолчанию, будет переполнение.

                  Вы считаете 0 при исключениях адекватным поведением?
                  Конкретно в случае с сахаром в виде Bindings.multiply/Bindings.divide, я считаю такое поведение оправданным. Если хочется самому обработать ошибку, то пожалуйста, есть Bindings.createIntegerBinding или его более низкоуровневый аналог IntegerBinding.

                  var width = new SimpleIntegerProperty();
                  var height = new SimpleIntegerProperty();
                  var area = Bindings.createIntegerBinding(
                          () -> {
                              try{
                                  return width.get() / height.get();
                              }
                              catch (ArithmeticException e){
                                  return ohNoTheHeightIsZero(width.get());
                              }
                          },
                          width, height // зависимости
                  );
                  var area = new IntegerBinding(){
                      {
                          bind(width, height); // зависимости
                      }
                      @Override
                      protected int computeValue() {
                          try{
                              return width.get() / height.get();
                          }
                          catch (ArithmeticException e){
                              return 42;
                          }
                      }
                  };
                  

                  И на всякий случай скажу, что мой первый комментарий был сарказмом. Там даже плашечка в конце стоит. Само собой, я не считаю JavaFX верхом развития реактивного программирования.


                  1. nin-jin
                    25.09.2023 11:27

                    Разве? Это включается каким-то флагом или как?

                    https://docs.oracle.com/javase/8/docs/api/java/lang/Math.html#multiplyExact-int-int-

                    я считаю такое поведение оправданным.

                    В следующий раз, когда банк-клиент вместо ошибки напишет, что у вас на счету 0 рублей, включите фронталку, вместе посарказмируем.


                    1. ValeryIvanov
                      25.09.2023 11:27
                      +1

                      Не очень красиво, перепрыгивать с вычисления площади, до расчёта баланса.
                      Само собой, при расчёте баланса нужно будет учитывать и возможность деления на ноль, и все варианты выхода за пределы диапазона int. Это не ответственность Bindings.multiply, $derived и, скорее всего, UI в целом.
                      Да и о каком переполнении в контексте баланса может идти речь, когда при любых важных расчётах с деньгами, необходимо использовать BigInteger/BigDecimal.


                      1. nin-jin
                        25.09.2023 11:27

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


    1. nin-jin
      25.09.2023 11:27
      -2

      Учитывая, что реактивность через $ сломана by design, мне очень тревожно за ваши проекты.


  1. rob2akhiyarov
    25.09.2023 11:27

    Тот же ref и computed из vue composition api? Или есть разница?


    1. nin-jin
      25.09.2023 11:27
      +1

      Есть, без компилятора не работает.


    1. Alexandroppolus
      25.09.2023 11:27

      Идеи восходят к фреймворку Knockout, ну а сейчас есть много где (тот же mobx, например)