# Подробнее о реактивности
Пришло время разобраться в теме поподробнее! Одной из отличительных особенностей Vue является ненавязчивая система реактивности. Модели представляют собой проксированные JavaScript-объекты. По мере их изменения обновляется и представление данных. Это делает управление состоянием приложения простым и интуитивно понятным. Тем не менее, у механизма реактивности есть ряд особенностей, знакомство с которыми позволит избежать распространённых ошибок. В этом разделе рассмотрим некоторые детали низкоуровневой реализации системы реактивности Vue подробнее.
# Что такое реактивность?
В последнее время этот термин часто встречается в программировании, но что это значит? Реактивность — концепция, позволяющая приспосабливаться к изменениям декларативным способом. Отличный канонический пример его демонстрации — электронная таблица Excel.
Если ввести цифру 2 в первую ячейку, а цифру 3 во вторую и, с помощью встроенной в Excel функции SUM, запросить их сумму — таблица её рассчитает. Ничего неожиданного. Но если изменить число в первой ячейке, то сумма тоже обновится автоматически.
JavaScript обычно так не работает — если написать что-то похожее на JavaScript, то это могло бы выглядеть так:
var val1 = 2
var val2 = 3
var sum = val1 + val2
// sum
// 5
val1 = 3
// sum
// 5
2
3
4
5
6
7
8
9
10
11
Если изменить первое значение, то сумма не обновится с его учётом.
Как же это сделать на JavaScript?
- Обнаружить изменение одного из значений
- Отследить функцию, которая их изменяет
- Вызвать функцию для обновления конечного результата
# Как Vue отслеживает изменения
При передаче простого JavaScript-объекта в экземпляр приложения или компонента в опции data
, Vue обойдёт все его свойства и преобразует их в Proxy (opens new window), используя обработчик с геттерами и сеттерами. Эта возможность доступна только в ES6, но есть версия Vue 3 с поддержкой старого подхода на основе Object.defineProperty
для поддержки браузеров IE. У обеих версий одинаковый API, но версия на Proxy легче и производительнее.
See the Pen Визуальное объяснение Proxy и реактивности во Vue by Vue (@Vue) on CodePen.
Это достаточно поверхностное объяснение, которое требует некоторых знаний о Proxy (opens new window) для понимания. Давайте немного углубимся. Есть достаточно много обучающих материалов про Proxy, но что действительно нужно знать, так это то, что Proxy — объект, который содержит в себе другой объект или функцию и позволяет «перехватывать» их.
Вот как это примерно используем: new Proxy(target, handler)
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop) {
return target[prop]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
Хорошо, пока что просто оборачиваем объект и возвращаем его. Круто, но пока не так уж и полезно. Но взгляните сюда, этот объект можно перехватить во время оборачивания его в Proxy. Этот перехват называется ловушкой.
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop) {
console.log('перехвачен!')
return target[prop]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// перехвачен!
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Кроме сообщения в консоль можно сделать всё что угодно. Можно даже не возвращать значение если потребуется. Это и делает Proxy настолько мощными для создания API.
У Proxy есть ещё одна особенность. Вместо того, чтобы просто возвращать значение в виде: target[prop]
, можно пойти дальше и воспользоваться Reflect
для корректной привязки this
. Получится примерно следующее:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop, receiver) {
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
Как упоминалось ранее, чтобы API обновил результат при каком-либо изменении, нужно установить новые значения. Давайте сделаем это в функции-обработчике track
, куда передаётся target
и key
.
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop, receiver) {
track(target, prop)
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Наконец, установим новые значения, когда что-то изменится. Для этого обновим эти данные с помощью нового Proxy, запустив эти изменения:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, prop, receiver) {
track(target, prop)
return Reflect.get(...arguments)
},
set(target, key, value, receiver) {
trigger(target, key)
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Помните этот список несколькими абзацами ранее? Теперь уже есть ответы на вопросы как Vue обрабатывает изменения:
Обнаружить изменение одного из значений: больше не нужно этого делать, т.к. Proxy позволяют перехватить его- Отследить функцию, которая их изменяет: это делается в геттере внутри Proxy, называемом
track
- Вызвать функцию для обновления конечного результата: это делается в сеттере внутри Proxy, называемом
trigger
Проксированный объект невидим для пользователя, но под капотом он позволяет Vue отслеживать зависимости и уведомлять о доступе к свойствам или их изменении. Начиная с Vue 3, система реактивности также доступна отдельным пакетом (opens new window). Есть нюанс, каким образом форматируются сообщения в консоли у разных браузеров, поэтому для более удобной работы с такими объектами удобнее использовать vue-devtools (opens new window).
# Проксированные объекты
Под капотом Vue отслеживает все объекты, которые были сделаны реактивными, поэтому для каждого объекта всегда возвращается свой Proxy.
Когда требуется доступ к вложенному объекту внутри реактивной Proxy, этот объект также преобразуется в Proxy перед возвращением.
const handler = {
get(target, prop, receiver) {
track(target, prop)
const value = Reflect.get(...arguments)
if (isObject(value)) {
return reactive(value)
} else {
return value
}
}
// ...
}
2
3
4
5
6
7
8
9
10
11
12
# Proxy vs оригинальная сущность
При использовании Proxy следует помнить — проксируемый объект не равен оригинальному объекту при строгом сравнении (===
). Например:
const obj = {}
const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
2
3
4
Оригинальный объект и обёрнутая в Proxy версия в большинстве случаев будут вести себя одинаково, но могут выдать неправильный результат в операциях со строгим сравнением, такими как .filter()
или .map()
. Эта особенность вряд ли возникнет при использовании Options API, потому что все свойства реактивного состояния вызываются из this
и уже гарантированно будут Proxy.
Однако, при использовании Composition API для явного создания реактивных объектов, хорошей практикой будет никогда не сохранять ссылку на оригинальный объект и работать только с реактивной версией.
const obj = reactive({
count: 0
}) // не ссылается на оригинал
2
3
# Наблюдатели
У каждого экземпляра компонента есть соответствующий экземпляр наблюдателя, который регистрирует любые свойства, «затронутые» во время отрисовки компонента в качестве зависимостей. Позже, при срабатывании сеттера зависимости, он уведомит наблюдателя, что приведёт к перерисовке компонента.
See the Pen Второе объяснение рактивности с Proxy во Vue 3 by Sarah Drasner (@sdras) on CodePen.
При передаче объекта данных в экземпляр компонента в качестве локальных данных, Vue преобразует его в Proxy. Этот Proxy позволит Vue отслеживать зависимости и получать уведомления об их изменениях. Каждое свойство рассматривается как зависимость.
После первой отрисовки у компонента будет список отслеживаемых зависимостей, полученный из свойств, затронутых в момент отрисовки. И наоборот, компонент подписывается на каждое из этих свойств. Когда Proxy перехватывает операцию обновления, все подписанные на свойство компоненты будут уведомлены и перерисованы.
При использовании Vue 2.x или более ранних версий, есть дополнительные особенности отслеживания изменений, которые подробнее рассмотрены здесь.