Сигналы обладают множеством преимуществ, включая встроенную гранулярную реактивность, простой API, лаконичный и понятный поток данных и многое другое.
Именно поэтому они используются практически во всех основных фреймворках, включая Solid, Qwix, Angular и Vue.
Они стали настолько популярны, что уже поступило предложение включить их в JavaScript.
Сигналы — мощный инструмент, но как именно он работает?
Рассмотрим механизм работы сигналов и упрощенные версии их реального применения. Хотя в данной статье приведен синтаксис SolidJs, все упомянутые концепции применимы к любому фреймворкам, использующему сигналы.
Начнем с изучения самой базовой единицы в области сигналов — Signal.
Signal
Signal — реактивная структура данных, используемая для управления состоянием внутри приложения.
Создадим Signal!
const [name, setName] = createSignal("John")
Создаем поле со значением (в данном случае «John») и перечнем подписчиков.
Возвращаем аксессор (в данном случае name
) и сеттер (setName
).
Аксессоры предоставляют способ доступа к данным внутри Signal — напрямую нельзя получить доступ к значению.
Сеттер используется для обновления данных в Signal, поскольку прямой доступ к ним запрещен.
Итак, у нас есть способ сохранения состояния, но нужен также способ реагирования на изменения в этом состоянии. Для этого будем использовать Effect.
Effect
Effect — инструмент для реагирования на изменения сигналов. Посмотрим, как он работает.
createEffect(() => { console.log(name()); });
Мы создали базовый Effect, который вызывает аксессор name
и выводит результат на консоль.
Созданный нами Effect содержит анонимную функцию.
Чтобы понять, что происходит при создании Effect, нужно иметь представление о контексте отслеживания.
Контекст отслеживания — глобальный стек среды выполнения, который помогает отслеживать, что выполняется в данный момент.
Когда мы создаем Effect, то помещаем его в контекст отслеживания:
После помещения Effect в контекст отслеживания выполняем содержащуюся в нем функцию.
Волшебство происходит при выполнении любого аксессора внутри createEffect
.
Запуская аксессор, осматриваем верхнюю часть контекста отслеживания. Затем помещаем Effect в верхнюю часть контекста отслеживания в качестве подписчика Signal, подключенного к аксессору.
(Желтая звездочка обозначает Effect).
После добавления Effect в качестве подписчика возвращаем значение, которое хранит Signal, в данном случае John
.
Затем выводим John
в консоль.
И удаляем Effect из контекста отслеживания:
Итак, выводим John в консоль. Что в этом такого? Чтобы понять это, нужно вызвать сеттер setName
.
Вызов сеттера
setName("Jane");
При вызове сеттера обновляем значение, хранящееся в Signal.
Затем проверяем список подписчиков, удаляем их из списка и вызываем каждого из них.
Заменяем значение внутри Signal на “Jane”
, удаляем Effect из списка, а затем вызываем его.
В данном случае речь идет об этом Effect:
createEffect(() => { console.log(name()); });
Он, в свою очередь, выводит “Jane”
и повторяет процесс регистрации себя в качестве подписчика исходного Signal.
У нас есть реактивная система, которая реагирует на изменения состояния.
Легко понять, как эту систему можно использовать для гранулярного UI-обновления, создавая Effect, который отображает компонент только при изменении определенного Signal.
Как видите, эта система может масштабироваться по мере того, как все больше компонентов подписываются на сигналы.
Все эти преимущества достигаются без дополнительных усилий со стороны разработчика — подписки обрабатываются автоматически. Одним словом: фантастика!
Мемоизация
Если бы я попросил вас найти произведение 12 * 16, вам пришлось бы немного подумать — возможно, вы удвоите 6 * 2, затем сложите числа и так далее.
Но если я спрошу вас снова, вы, скорее всего, без лишних усилий ответите 192.
Иногда полезно вспомнить уже проделанные вычисления, чтобы не повторять их.
Эта логика применима к нам, а также к Signal’ам. Мы можем использовать функцию createMemo
, чтобы реализовать этот паттерн с помощью Signal’ов.
const [num, setNum] = createSignal(1); const memo = createMemo(() => { return num() * 2; });
Что происходит при вызове функции createMemo
?
Сначала создаем Memo с массивом подписчиков аналогично тому, как работаем с Signal’ами.
Затем помещаем специальную функцию-обертку M: f()
в контекст отслеживания точно так же, как делали это с Effect.
Затем вызываем функцию, переданную в качестве входа в createMemo
. Когда она выполняется, вызывает num()
, делая M: f()
подписчиком Signal.
Затем сохраняем результат в Memo.
Это означает, что можно всегда возвращать сохраненное значение и не пересчитывать его до тех пор, пока Signal, на который мы подписались, не изменится.
Посмотрим, что произойдет при использовании memo()
(возвращаемое значение createMemo
).
createEffect(() => { console.log("effect: ", memo()); });
Мы создали Effect, вызывающий функцию memo
.
И снова видим ту же схему, что и с аксессорной функцией Signal: когда она вызывается, смотрим на вершину области отслеживания и регистрируем Effect как подписчика на Memo.
Затем выводим effect: 2
, используя мемоизированное значение без повторного вычисления.
Посмотрим, как все это будет выглядеть при изменении значения Signal.
setNum(2);
Изменив значение Signal на 2, мы вызываем всех подписчиков — в данном случае функцию-обертку memo.
const [num, setNum] = createSignal(1); const memo = createMemo(() => { return num() * 2; });
Повторный запуск функции приводит к следующему:
- функция-обертка Memo перерегистрируется в качестве подписчика Signal;
- значение, сохраненное в Memo, обновляется до 4;
- вызываем всех подписчиков Memo.
Вызов подписчиков Memo приводит к запуску Effect и выводу effect: 4
.
Все подписчики возвращаются на место для следующего изменения Signal.
Memo позволяет создавать граф реактивности такого размера, какой нам нужен, с увеличением производительности для мемоизации тяжелых вычислений.
Не забывайте использовать createMemo
, чтобы избежать многократного пересчета тяжелых вычислений.
Подведем итоги
Мы рассмотрели, как работают Signal’ы с их механизмом автоматической подписки.
Потом изучили, как работают Effect’ы и подписываются на Signal’ы с помощью области отслеживания.
Затем узнали, как работает createMemo
и как его можно использовать, чтобы избежать пересчета тяжелых вычислений.
Signal — мощный и потому популярный инструмент в мире фронтенда. Теперь, когда вы поняли, как работает эта структура, можете создавать с ее помощью потрясающие приложения.
Читайте также:
- Производительность фронтенда: лав-стори для разработчиков
- Чистая архитектура фронтенда: 7 советов для достижения успеха
- Представляем SafeTest: новый подход к тестированию фронтенда
Читайте нас в Telegram, VK и Дзен
Перевод статьи Matan Cohen: Signals behind the scenes