Реактивность vue как работает
Реактивность Vue.js
Реактивность фреймворка Vue.js и использование метода Vue.set
В статье рассматривается система реактивности Vue.js и приводится пример того, когда эта система не работает.
Также даются два способа решения этой проблемы, один из которых — использование встроенного метода Vue.set.
Одна из возможностей, за которую мы любим Vue.js— это система реактивности. Для наглядного примера такой реактивности Vue.js можно привести нижеследующий код.
В этом примере первоначально в шаблоне генерируется вывод текста “Hello World”.
Затем по истечении 2 секунд текст в шаблоне меняется на “Goodbye World”:
В приведенном выше примере ключ message объекта data является реактивным.
Это означает, что при любом изменении значения ключа message происходит автоматическое изменение выводимого значения в шаблоне.
Ловушка системы реактивности
Такая автоматизация предоставляет ряд несомненных преимуществ при создании приложений на Vue.js:
Другими словами — делает жизнь разработчика более простой.
Однако, такая простота имеет свою обратную сторону медали. Подвох в данном случае подобен аналогии автомобилю с автопилотом.
Автоматическая реактивность делает нас, как разработчиков, ленивыми. И когда что-то работает не так, как мы бы этого ожидали — мы не знаем, в чем причина того, что так произошло.
Когда реактивность оборачивается злом
Один из студентов курса Ultimate Vue.js Developers столкнулся с интересной проблемой и поделился ею со мной. Этот студент работает над проектом Vue.js Poster Shop — первый учебный проект данного курса.
В этом проекте есть задача — создание объектов shopping cart при помощи Vue.js. Такой объект cart отображается в действующем проекте как обычный JavaScript-объект:
Логика этого метода следующая:
Появляется проблема
Коллекция объектов cart представляет из себя простой список объектов в шаблоне:
Проблема заключается в том, что в шаблоне значение ключа quantity всегда будет выводиться как 1.
Другими словами, вне зависимости от того, изменилось ли значение ключа quantity или просто этому ключу было присвоено значение 1 — в любом случае в шаблоне будет выведено значение 1.
Моей первой мыслью было, что логика метода addToCard имеет в себе ошибку.
Так что проблема была не здесь.
Как реактивность работает “под капотом”
Как уже упоминалось ранее, побочным следствием автоматической работы системы реактивности Vue.js является тот факт, что если код работает не так, как было задумано, разработчику трудно понять, что является причиной такого поведения.
Однако, такой ситуации можно избежать, если понимать, как в действительности работает система реактивности Vue.js.
Getters и setters
Стандартное поведение JavaScript-объекта, когда производится извлечение значения или изменение значения какого-либо ключа этого объекта — вопрос однозначный.
К примеру, имеется такой JavaScript-объект:
Извлечение значения ключа a объекта myObj можно выполнить таким образом:
Для более подробной информации об обоих псевдо-свойствах можно перейти по ссылкам — getter и setter.
Такой набор getter’ов и setter’ов позволяет Vue отслеживать любое изменение в любом свойстве и автоматически отображать эти изменения в шаблоне.
Метод reactiveSetter()
Вернемся назад, к JavaScript-объекту myProduct :
После того, как экземпляр Vue был инициализирован, можно открыть консоль браузера и увидеть, что для каждого ключа этого объекта Vue.js создал пару getter/setter:
Эти функции getter и setter выполняют большой объем работы (посмотрите исходный код).
Подробнее
Vue.js является отличным фреймворком, за исключением одного случая.
Если в существующий объект добавить или удалить свойство после того, как этот объект был инициализирован системой Vue.js, то Vue ничего не будет знать об этом свойстве.
Например, добавим в JavaScript-объект myProduct новое свойство quantity после того, как Vue.js выполнил инициализацию этого объекта:
Обновление реактивных объектов
В примере с объектом myProduct проблему обновления реактивного объекта можно решить двумя способами.
Первый способ — это создать новый объект и затем добавить его в коллекцию уже существующих объектов.
Такой подход дает гарантию того, что у Vue.js будет возможность создать пару getter/setter для каждого свойства вновь создаваемого объекта.
Метод Vue.set
Данный метод гарантирует, что для вновь создаваемого свойства в уже существующем реактивном объекте будут создана пара getter/setter и данное свойство также станет реактивным.
Метод Vue.set добавит в объект item новое свойство quantity и автоматически создаст для этого свойства пару getter/setter, тем самым сделав свойство quantity реактивным:
Массивы
Подобно объектам, массивы также являются реактивными в системе Vue.js и благодаря этому могут отслеживать факт наличия изменений.
Также как и с объектами, в массив нельзя добавить новый элемент “напрямую” и ожидать реактивности от нового элемента.
Например, добавление в массив нового элемента по индексу не даст желаемого результата:
Подробно о реактивности
Мы уже разобрали большую часть основ, так что пришло время нырнуть поглубже! Одна из наиболее примечательных возможностей Vue — это ненавязчивая реактивность. Модели представляют собой простые JavaScript-объекты. По мере их изменения обновляется и представление данных, благодаря чему управление состоянием приложения становится простым и очевидным. Тем не менее, у механизма реактивности есть ряд особенностей, знакомство с которыми позволит избежать появление распространённых ошибок. В этом разделе руководства мы подробно рассмотрим низкоуровневую реализацию системы реактивности Vue.
Как отслеживаются изменения
Геттеры и сеттеры не видны пользователю, но именно они являются тем внутренним механизмом, который позволяет Vue отслеживать зависимости и изменения данных. К сожалению, при таком подходе выведенные в консоль браузера геттеры и сеттеры выглядят не так, как обычные объекты, поэтому для более наглядной визуализации лучше использовать инструменты разработчика Vue-devtools.
В каждый экземпляр компонента добавлен связанный с ним экземпляр наблюдателя, который помечает все поля, затронутые при отрисовке компонента, как зависимости. В дальнейшем, когда вызывается сеттер поля, помеченного как зависимость, этот сеттер уведомляет наблюдателя, который, в свою очередь, инициирует повторную отрисовку компонента.
Особенности отслеживания изменений
Вследствие ограничений JavaScript, есть виды изменений, которые Vue не может обнаружить. Однако существуют способы обойти их, чтобы сохранить реактивность.
Для объектов
Vue не может обнаружить добавление или удаление свойства. Так как Vue добавляет геттер/сеттер на этапе инициализации экземпляра, свойство должно присутствовать в объекте data для того чтобы Vue преобразовал его и сделал реактивным. Например:
Во Vue нельзя динамически добавлять новые корневые реактивные свойства в уже существующий экземпляр. Тем не менее, можно добавить реактивное свойство во вложенные объекты, используя метод Vue.set(object, propertyName, value) :
Для массивов
Vue не может отследить следующие изменения в массиве:
Для решения второй проблемы используйте splice :
Объявление реактивных свойств
Поскольку Vue не позволяет динамически добавлять корневые реактивные свойства, это означает, что все корневые поля необходимо инициализировать изначально, хотя бы пустыми значениями:
Если не задать поле message в опции data, Vue выведет предупреждение, что функция отрисовки пытается получить доступ к несуществующему свойству.
Существуют технические причины для этого ограничения: оно позволяет исключить целый класс граничных случаев в системе учёта зависимостей, а также упростить взаимодействие Vue с системами проверки типов. Но более важно то, что с этим ограничением становится проще поддерживать код, так как теперь объект data можно рассматривать как схему состояния компонента. Код, в котором реактивные свойства компонента перечислены заранее, намного проще для понимания.
Асинхронная очередь обновлений
Понимание реактивности во Vue.js (Шаг за Шагом)
В этой статье я собираюсь рассказать о реактивности во Vue. Начнем с определения, что такое реактивность.
Для меня реактивность означает, что когда изменилось состояние одного объекта, то все использующие его значения другие объекты то же автоматически обновились.
Если это попытаться объяснить в математическом определении, то у нас будет уравнение, в котором y = x + 1, x — наша независимая переменная, а y — наша зависимая переменная. Почему у называется зависимой переменной? Ну, потому что его значение зависит от значения х!
Хорошо известным примером системы реактивности является электронная таблица.
Шаг 1
Давайте забудем о фреймворках и шаг за шагом реализуем их в vanilla javascript. Для начала давайте сохраним нашу переменную x в объекте состояния (позже я объясню, почему она находится внутри объекта). Затем создадим функцию для отображения значения y в html. Наконец, вручную запустим функцию:
Шаг 2
Теперь давай те сделаем так что бы вместо ручного вызова функции renderY, у нас бы была бы функция setState, которая сама обновляла бы наше состояние и запускала бы функцию render.
В итоге мы получили React! 🙂
Но есть ли способ, которым мы можем вызвать функцию рендеринга автоматически, когда мы обновляем состояние?
Метод defineProperty для Object является функцией только для es5+. Вот почему vue 2 не поддерживает IE8 и ниже.
Это API позволяет нам переопределить поведение некоторых объектов по умолчанию, например, геттеров (getter) и сеттеров (setter).
Также именно поэтому мы хотим, чтобы состояние (state) было объектом. И это также объясняет, почему у vue есть некоторые махинации с массивами. Потому что это API отсутствует в массивах.
Давайте вызовем нашу функцию рендеринга в сеттере!
Мы берем значение x в начале, затем в геттере просто возвращаем его значение и в сеттере запускаем нашу функцию рендеринга после обновления значения x.
Это работает, но наш код выглядит так ужасно. Он работает только для свойства «x» и не работает для вложенного объекта. Давайте проведем рефакторинг нашего кода.
Шаг 3
Давайте создадим наблюдаемую функцию. Эта функция будет просто перебирать все ключи объекта и рекурсивно делает все вложенные объекты наблюдаемыми.
Из демонстрации, которую вы можете увидеть в этом особом случае, наш код все еще работает. Тем не менее, при обновленном состоянии наш код может выполнять только одну отдельную работу — рендеринг Y, и он всегда будет рендерить Y, независимо от того, какое состояние обновлено. Я считаю, что это ошибка. Поэтому мы должны найти способ заставить наше наблюдаемое состояние отслеживать, какой код на самом деле зависит от того, от какого состояния.
Шаг 4
Чтобы отслеживать зависимости кода, давайте создадим простой класс Dep, чтобы мы могли использовать его для создания экземпляра dep для каждого ключа объекта.
Как вы можете видеть, этот класс довольно прост: в конструкторе мы просто создаем набор заданий jobs, причина использования Set в том, что мы не хотим добавлять дубликаты заданий для одного и того же ключа.
Метод depend просто добавляет текущее задание job в набор заданий jobs а метод notify запускает все добавленные задания.
Обратите внимание, что у нас также есть статическая переменная job для хранения текущего оцениваемого задания.
Теперь давайте используем класс Dep для обновления нашей наблюдаемой функции.
Для каждого ключа мы создаем новый экземпляр класса Dep для отслеживания зависимой job.
Но как мы узнаем, зависит ли текущее задание от этого свойства объекта? Правильно, нам просто нужно запустить это задание один раз (что мы уже делали в предыдущих демонстрациях), когда задание запускается, если задание пытается получить доступ к этому свойству объекта, мы знаем, что это задание зависит от этого свойства. Поэтому мы можем вызвать dep.depend(), чтобы добавить текущее задание к экземпляру Dep внутри геттера.
И затем, когда это свойство обновляется, мы хотим перезапустить все сохраненные задания, вызвав dep.notify() внутри сеттера.
Это может немного смущать вас, но давайте просто соберем все вместе в демоверсии, чтобы вы могли видеть, как все происходит более четко.
Таким образом, как вы можете видеть, при запуске задания renderY функция пытается получить state.x, поэтому будет запущен метод геттер для x, поэтому задание renderY будет добавлено в job, установленные для x.
Шаг 5
В нашем демо у нас есть только одна работа, renderY. Теперь я хочу получить другую job для renderX, но я не хочу копировать и вставлять код для установки текущей job; потом снова запусткать job, а затем очищать job.
Опять же, как хороший разработчик, мы всегда должны помнить о принципе DRY.
Итак, мы можем создать простую функцию раннера, которая сделает всю эту грязную работу за нас.
Давайте вернемся к нашему codepen и добавим больше раннеров;
К настоящему времени мы только что внедрили очень базовую версию системы реактивности vue. Конечно, настоящий код vue гораздо сложнее, чем этот. Потому что нужно рассмотреть множество исключений и крайних случаев, а код должен быть более организованным.
Но если вы захотите проверить исходный код vue, вы увидите класс Dep, функцию, которая делает объект реактивным, и класс наблюдателя (watcher), выполняющий в основном ту же работу, что и наша простая функция runner.
В целом, эта система, основанная на API object.defineProperty, работает нормально в большинстве случаев. Но каковы особенности этой системы? Помните, я говорил, что API object.defineProperty не работает с массивом? Vue сделал переопределение некоторых методов-прототипов, таких как array.push, чтобы сделать массив реактивным. Другое дело, что таким образом мы только активируем начальные свойства объекта, а как насчет новых свойств, которые вы добавите к объекту позже? Vue не может знать о новом свойстве заранее, поэтому оно не может сделать его реактивным. Поэтому каждый раз, когда вы хотите добавить новое реактивное свойство к объекту данных, вы должны использовать Vue.set.
Тем не менее, всех этих махинаций больше не будут в Vue 3.0. Потому что система реактивности Vue 3.0 будет полностью переписана с помощью Object Proxy. В настоящее время Vue 3.0 все еще находится в активной стадии разработки, и мы пока не не можем увидить исходный код. Однако Evan You, создатель Vue, объяснил, как это будет работать на семинаре, который я посетил на конференции Vue в Торонто несколько месяцев назад. Сейчас я покажу вам, ребята, что я узнал из этого.
Еще кое-что…
Proxy — это новый глобальный API, представленный в ES6. Это функция уровня языка, которая означает, что это не полифиллинг, а также означает, что Vue 3.0 по умолчанию не поддерживает IE11 и ниже. Однако вы можете настроить свою сборку для использования старой системы реактивности на основе object.defineProperty.
Если вы ранее не слышали о Proxy API, вы можете прочитать какой-нибудь документ об этом на одном MDN или в вашем любимом w3school.
Прежде всего, давайте определим наши traps. В геттере trap мы создаем deps, если он еще не создан в хранилище, далее мы вызываем dep.depend(), чтобы зарегистрировать текущее задание как зависимость, как мы это делали раньше. То же самое в сеттере trap, мы обновляем значение в target, а затем вызываем dep.notify() для запуска job.
Затем мы обновляем нашу наблюдаемую функцию, чтобы использовать Proxy с нашими обработчиками. Мы создаем WeakMap для хранения наблюдаемого объекта, поэтому мы не будем наблюдать один и тот же объект несколько раз.
Заключение
В этой статье я рассказал как устроена реактивность в Vue 2 и как будет устроена в будущей версии Vue 3. Если у вас есть вопросы или свое мнение на этот счет добро пожаловать, в комментарий автора статьи.
# Основы реактивности
# Объявление реактивного состояния
Реактивное состояние из объекта JavaScript создаётся с помощью метода reactive :
Метод reactive — эквивалент Vue.observable() API во Vue 2.x, переименованный чтобы избежать путаницы с observables из RxJS. Возвращаемое состояние здесь будет являться реактивным объектом. Реактивное преобразование «глубокое» — оно будет влиять на все вложенные свойства переданного объекта.
Основной сценарий использования реактивного состояния во Vue — использование во время отрисовки. Благодаря отслеживанию зависимостей, представление будет обновляться автоматически при изменениях реактивного состояния.
Подробнее о методе reactive можно узнать в разделе Основы API реактивности.
# Создание автономных ссылок на реактивные значения
ref вернёт реактивный объект, который можно изменять и который служит реактивной ссылкой (ref, от слова reference) для внутреннего значения, которое он хранит — откуда и происходит его имя. Этот объект содержит только одно свойство с именем value :
# Разворачивание ref-ссылок
Если обращаться к реальному экземпляру объекта не потребуется, то можно обернуть его в метод reactive :
# Доступ в реактивных объектах
При доступе к ref или мутировании как свойства реактивного объекта, автоматически она будет разворачиваться во внутреннее значение и вести себя как обычное свойство:
Если новая ссылка присваивается к свойству, связанному с существующей ссылкой, то она просто заменит собой старую ссылку:
# Деструктурирование реактивного состояния
При необходимости использовать лишь несколько свойств из большого реактивного объекта, может возникнуть соблазн воспользоваться деструктурированием из ES6
(opens new window) для получения нужных свойств:
К сожалению, при таком деструктурировании реактивность обоих свойств будет потеряна. Для таких случаев необходимо сначала преобразовать реактивный объект в набор ссылок. Эти ссылки сохранят реактивную связь с исходным объектом:
Подробнее о refs можно узнать в разделе API реактивных ссылок.
# Предотвращение изменений реактивных объектов с помощью readonly
Иногда необходимо отслеживать изменения реактивного объекта ( ref или reactive ), но при этом запретить его изменения из определённого места приложения. Например, когда используем реактивный объект, внедряемый через provide, и хотим предотвратить попытки изменений там, где он будет внедряться. Для таких случаев можно создать прокси «только для чтения» исходного объекта:
(opens new window)
Последнее обновление страницы: около 1 месяца назад
# Подробнее о реактивности
Настало время разобраться в теме поподробнее! Одной из отличительных особенностей Vue является его ненавязчивая система реактивности. Модели представляют собой проксированные JavaScript-объекты. По мере их изменения обновляется и представление данных. В итоге управление состоянием приложения становится простым и интуитивно понятным. Тем не менее, у механизма реактивности есть ряд особенностей, понимание которых позволит избежать распространённых ошибок. В этом разделе рассмотрим подробнее некоторые детали низкоуровневой реализации системы реактивности Vue.
# Что такое реактивность?
В последнее время этот термин часто встречается в программировании, но что он значит? Реактивность — концепция, которая позволяет приспосабливаться к изменениям декларативным способом. Отличный канонический пример для демонстрации — электронная таблица Excel.
Браузер не поддерживает тег video.
Если ввести цифру 2 в первую ячейку, а цифру 3 во вторую, а затем, с помощью встроенной в Excel функции SUM, запросить их сумму — таблица её рассчитает. Ничего неожиданного. Но если изменить число в первой ячейке, то сумма обновится автоматически.
Обычно JavaScript так не работает. Если попробовать реализовать похожее на JavaScript, то это могло бы выглядеть так:
Но если изменить первое число, то сумма не пересчитается с его учётом.
Как же этого добиться на JavaScript?
С высоты птичьего полёта, должна быть возможность сделать несколько вещей:
Используя код предыдущего примера, нет никакой возможности сделать это. Вернёмся к нему чуть позже, чтобы разобрать как можно адаптировать его для совместимости с системой реактивности Vue.
Сначала разберёмся как Vue реализует перечисленные выше требования к реактивности.
# Как Vue определяет какой код выполнялся
Чтобы получить возможность пересчитывать сумму всякий раз, когда значения изменятся, первое что потребуется — обернуть этот код в функцию:
Но каким образом расскажем Vue об этой функции?
Vue с помощью эффекта отслеживает какая функция в данный момент была запущена. Эффект — обёртка вокруг функции, которая начинает отслеживание непосредственно перед её вызовом. Таким образом Vue знает, какой эффект выполняется в любой заданной точке, и может при необходимости запустить его снова.
Чтобы лучше это понять, давайте попробуем реализовать нечто подобное самостоятельно, без Vue, и посмотреть как это может работать.
Что для начала требуется, так это что-то, что может обернуть вычисление суммы вот так:
Эффекты выступают в качестве стартовой точки для многих возможностей. Например, как отрисовка компонента, так и вычисляемые свойства внутри используют эффекты. Каждый раз, когда что-то «волшебным» образом реагирует на изменение данных, можно быть уверенным что это было обёрнуто в эффект.
Но понимание какой код выполняется — лишь одна из частей головоломки. Как же Vue узнаёт какие значения использует эффект и как определяет что они изменились?
# Как Vue отслеживает изменения
Отследить переназначение локальных переменных в предыдущих примерах не получится, такого механизма просто нет в JavaScript. Можно лишь отслеживать изменения свойств объектов.
Поэтому, когда возвращается простой объект JavaScript из функции data компонента, Vue обернёт его в Proxy
Демо выше — достаточно поверхностное объяснение, которое требует некоторых знаний о Proxy
(opens new window) для понимания. Давайте немного углубимся. Есть много обучающих материалов о Proxy, но самое важное что нужно знать это то, что Proxy — объект, который содержит в себе другой объект или функцию и позволяет «перехватывать» их.
Они используются вот так: new Proxy(target, handler)
В примере показано как можно перехватить попытку прочитать свойство отслеживаемого объекта. Подобную функцию обработчика, также называют ловушкой. Есть несколько разных типов ловушек, каждая из которых обрабатывает свой тип взаимодействия.
Кроме сообщения в консоль можно сделать всё что угодно. Можно даже не возвращать значение, если это потребуется. Это и делает Proxy настолько мощными для создания API.
Наконец, необходимо повторно запустить эффект при изменении значения свойства. Для этого воспользуемся обработчиком set в прокси:
Помните этот список? Теперь уже есть некоторые ответы, как во Vue реализуются эти шаги:
Проксируемый объект невидим для пользователя, но под капотом он позволяет Vue отслеживать зависимости и уведомлять о считывании свойств или их изменениях. Небольшое отличие лишь в том, что проксированные объекты при выводе в консоль форматируются немного иначе, поэтому рекомендуем установить vue-devtools
Если переписать оригинальный пример с использованием компонента, то получится так:
Начиная с Vue 3, реактивность теперь доступна как отдельный пакет
# Проксированные объекты
Под капотом Vue отслеживает все объекты, которые были сделаны реактивными, поэтому для каждого объекта всегда возвращается свой Proxy.
Когда требуется доступ к вложенному объекту внутри реактивной Proxy, этот объект перед возвращением также преобразуется в Proxy.