Что такое реактивность?
В наши дни этот термин довольно часто встречается в программировании, но что люди имеют в виду, когда говорят его? Реактивность — это парадигма программирования, которая позволяет нам декларативно приспосабливаться к изменениям.
Как работает реактивность в Vue
Мы не можем отслеживать чтение и запись локальных переменных, как в примере. В ванильном JavaScript просто нет механизма для этого. Однако мы можем перехватить чтение и запись свойств объекта
В JavaScript существует два способа перехвата доступа к свойствам: геттеры / сеттеры и прокси . Vue 2 использовал геттеры/сеттеры исключительно из-за ограничений поддержки браузера. В Vue 3 прокси используются для реактивных объектов, а геттеры / сеттеры — для ссылок. Вот некоторый псевдокод, иллюстрирующий, как они работают
Важно
Фрагменты кода здесь и ниже предназначены для объяснения основных концепций в максимально простой форме, поэтому многие детали опущены, а крайние случаи игнорируются.
Это объясняет некоторые ограничения реактивных объектов
- Когда вы присваиваете или деструктурируете свойство реактивного объекта локальной переменной, доступ к этой переменной или присвоение ей является нереактивным, поскольку оно больше не запускает ловушки прокси-сервера get/set на исходном объекте. Обратите внимание, что это «отключение» влияет только на привязку переменной — если переменная указывает на непримитивное значение, такое как объект, изменение объекта все равно будет реактивным.
- Возвращенный прокси из reactive(), хотя и ведет себя так же, как оригинал, имеет другую идентичность, если мы сравниваем его с оригиналом с помощью оператора ===
Внутри track() мы проверяем, есть ли текущий эффект. Если он есть, мы ищем эффекты подписчика (хранящиеся в наборе) для отслеживаемого свойства и добавляем эффект в набор:
Подписки на эффекты хранятся в глобальной WeakMap target, Mapkey, Seteffect структуре данных. Если для свойства не найден набор эффектов подписки (отслеживается впервые), он будет создан. getSubscribersForProperty() Вкратце, это то, что делает функция. Для простоты мы опустим его детали.
Внутри trigger() мы снова ищем эффекты подписчика для свойства. Но на этот раз вместо этого мы вызываем их:
Теперь вернемся к whenDepsChange() функции:
Он оборачивает необработанную updateфункцию в эффект, который устанавливается в качестве текущего активного эффекта перед запуском фактического обновления. Это позволяет track() вызовам во время обновления найти текущий активный эффект.
На этом этапе мы создали эффект, который автоматически отслеживает свои зависимости и повторно запускается при изменении зависимости. Мы называем это реактивным эффектом
Vue предоставляет API, который позволяет создавать реактивные эффекты: watchEffect(). На самом деле, вы, возможно, заметили, что это работает очень похоже на магию whenDepsChange() в примере. Теперь мы можем переработать исходный пример, используя реальные API Vue:
Использование реактивного эффекта для изменения ссылки — не самый интересный вариант использования — на самом деле использование вычисляемого свойства делает его более декларативным:
Внутренне computed управляет его аннулированием и повторным вычислением, используя реактивный эффект.
Так каков пример распространенного и полезного реактивного эффекта? Что ж, обновляем DOM! Мы можем реализовать простой «реактивный рендеринг» следующим образом:
Фактически, это очень похоже на то, как компонент Vue синхронизирует состояние и DOM — каждый экземпляр компонента создает реактивный эффект для рендеринга и обновления DOM. Конечно, компоненты Vue используют гораздо более эффективные способы обновления DOM, чем innerHTML
Время выполнения и реактивность во время компиляции
Система реактивности Vue в основном основана на среде выполнения: все отслеживание и запуск выполняются во время выполнения кода непосредственно в браузере. Плюсы реактивности во время выполнения заключаются в том, что она может работать без этапа сборки и меньше крайних случаев. С другой стороны, это накладывает ограничения на синтаксис JavaScript, что приводит к необходимости использования контейнеров значений, таких как ссылки Vue.
Некоторые фреймворки, такие как Svelte , предпочитают преодолевать такие ограничения, реализуя реактивность во время компиляции. Он анализирует и преобразует код, чтобы имитировать реактивность. Этап компиляции позволяет платформе изменять семантику самого JavaScript — например, неявно внедряя код, который выполняет анализ зависимостей и запускает эффекты, связанные с доступом к локально определенным переменным. Обратной стороной является то, что такие преобразования требуют этапа сборки, а изменение семантики JavaScript по сути означает создание языка, который выглядит как JavaScript, но компилируется во что-то другое.
Команда Vue исследовала это направление с помощью экспериментальной функции под названием Reactivity Transform, но в конце концов мы решили, что она не подходит для проекта из-за приведенных
# Отладка реактивности
Замечательно, что система реактивности Vue автоматически отслеживает зависимости, но в некоторых случаях нам может потребоваться выяснить, что именно отслеживается или что вызывает повторный рендеринг компонента.
# Хуки отладки компонентов
Мы можем отладить, какие зависимости используются во время рендеринга компонента и какая зависимость запускает обновление, используя хуки жизненного цикла и . Оба перехватчика получат событие отладчика, содержащее информацию о рассматриваемой зависимости. Рекомендуется поместить оператор в обратные вызовы для интерактивной проверки зависимости onRenderTracked onRenderTriggered debugger
Важно
Перехватчики отладки компонентов работают только в режиме разработки.
Объекты событий отладки имеют следующий тип:
# Вычислительная отладка
Мы можем отлаживать вычисляемые свойства, передавая computed() второй объект параметров с помощью onTrack и onTrigger обратных вызовов:
- onTrack будет вызываться, когда реактивное свойство или ссылка отслеживается как зависимость.
- onTrigger будет вызываться, когда обратный вызов наблюдателя инициируется мутацией зависимости.
Оба обратных вызова будут получать события отладчика в том же формате , что и перехватчики отладки компонентов:
Обратите внимание
onTrack & onTrigger вычисляемые параметры работают только в режиме разработки.
# Отладка наблюдателя
Подобно computed(), наблюдатели также поддерживают параметры onTrack и onTrigger
Интеграция с внешними государственными системами
Система реактивности Vue работает путем глубокого преобразования простых объектов JavaScript в реактивные прокси. Глубокое преобразование может быть ненужным, а иногда и нежелательным при интеграции с внешними системами управления состоянием (например, если внешнее решение также использует прокси).
Общая идея интеграции системы реактивности Vue с решением для управления внешним состоянием состоит в том, чтобы хранить внешнее состояние в файле shallowRef. Неглубокая ссылка реагирует только при .value доступе к ее свойству — внутреннее значение остается нетронутым. Когда внешнее состояние изменится, замените значение ref, чтобы запустить обновления.
Неизменяемые данные
Если вы реализуете функцию отмены/повтора, вам, вероятно, захочется делать снимок состояния приложения при каждом редактировании пользователем. Однако система изменяемой реактивности Vue не лучше всего подходит для этого, если дерево состояний велико, поскольку сериализация всего объекта состояния при каждом обновлении может быть дорогостоящей с точки зрения затрат как на процессор, так и на память.
Неизменяемые структуры данных решают эту проблему, никогда не изменяя объекты состояния — вместо этого они создают новые объекты, которые имеют одинаковые неизмененные части со старыми. Существуют разные способы использования неизменяемых данных в JavaScript, но мы рекомендуем использовать Immer с Vue, поскольку это позволяет использовать неизменяемые данные, сохраняя при этом более эргономичный и изменяемый синтаксис.
Мы можем интегрировать Immer с Vue с помощью простой компоновки:
State Machines
это модель для описания всех возможных состояний, в которых может находиться приложение, и всех возможных способов перехода из одного состояния в другое. Хотя это может быть излишним для простых компонентов, это может помочь сделать сложные потоки состояний более надежными и управляемыми.
Одной из самых популярных реализаций конечного автомата в JavaScript является XState. Вот составной компонент, который с ним интегрируется:
# RxJS
RxJS — библиотека для работы с асинхронными потоками событий. Библиотека VueUse предоставляет @vueuse/rxjs надстройку для подключения потоков RxJS к системе реактивности Vue.
Подключение к сигналам
Многие другие фреймворки ввели примитивы реактивности, похожие на ссылки из API композиции Vue, под термином «сигналы»:
По сути, сигналы — это тот же тип примитива реактивности, что и ссылки Vue. Это контейнер значений, который обеспечивает отслеживание зависимостей при доступе и срабатывание побочных эффектов при мутации. Эта парадигма, основанная на реактивных примитивах, не является особенно новой концепцией в мире внешнего интерфейса: она восходит к таким реализациям, как Knockout observables и Meteor Tracker , появившимся более десяти лет назад. API Vue Options и библиотека управления состоянием React MobX также основаны на тех же принципах, но скрывают примитивы за свойствами объекта.
Хотя это не является обязательным признаком для того, чтобы что-то можно было квалифицировать как сигнал, сегодня эта концепция часто обсуждается вместе с моделью рендеринга, где обновления выполняются посредством детальных подписок. Из-за использования Virtual DOM Vue в настоящее время полагается на компиляторы для достижения аналогичной оптимизации . Однако мы также изучаем новую стратегию компиляции, вдохновленную Solid (режим Vapor), которая не полагается на Virtual DOM и использует больше преимуществ встроенной системы реактивности Vue.
# Компромиссы при проектировании API
Дизайн сигналов Preact и Qwik очень похож на smallRef в Vue: все три предоставляют изменяемый интерфейс через .value свойство. Мы сосредоточим обсуждение на сигналах Solid и Angular.
# Твердые сигналы
В дизайне API Solid createSignal() особое внимание уделяется разделению чтения и записи. Сигналы предоставляются как геттер, доступный только для чтения, и отдельный сеттер:
Обратите внимание, как count сигнал может передаваться без установщика. Это гарантирует, что состояние никогда не сможет быть изменено, если только установщик не будет явно предоставлен. Оправдывает ли эта гарантия безопасности более подробный синтаксис, может зависеть от требований проекта и личного вкуса — но если вы предпочитаете этот стиль API, вы можете легко воспроизвести его в Vue:
# Угловые сигналы
Angular претерпевает некоторые фундаментальные изменения: отказ от грязных проверок и введение собственной реализации примитива реактивности. API Angular Signal выглядит следующим образом:
Опять же, мы можем легко скопировать API в Vue:
По сравнению с ссылками Vue, стиль API Solid и Angular, основанный на геттерах, обеспечивает некоторые интересные компромиссы при использовании в компонентах Vue:
- () немного менее подробно, чем .value, но обновление значения более подробно.
- Развертывание ссылок отсутствует: для доступа к значениям всегда требуется (). Это делает доступ к значениям единообразным повсюду. Это также означает, что вы можете передавать необработанные сигналы в качестве свойств компонента.
Подходят ли вам эти стили API, в некоторой степени субъективно. Наша цель — продемонстрировать основное сходство и компромиссы между этими различными конструкциями API. Мы также хотим показать, что Vue является гибким: вы не привязаны к существующим API. При необходимости вы можете создать свой собственный примитивный API реактивности для удовлетворения более конкретных потребностей.