# Подробнее о реактивности
Настало время разобраться в теме поподробнее! Одной из отличительных особенностей Vue является его ненавязчивая система реактивности. Модели представляют собой проксированные JavaScript-объекты. По мере их изменения обновляется и представление данных. В итоге управление состоянием приложения становится простым и интуитивно понятным. Тем не менее, у механизма реактивности есть ряд особенностей, понимание которых позволит избежать распространённых ошибок. В этом разделе рассмотрим подробнее некоторые детали низкоуровневой реализации системы реактивности Vue.
# Что такое реактивность?
В последнее время этот термин часто встречается в программировании, но что он значит? Реактивность — концепция, которая позволяет приспосабливаться к изменениям декларативным способом. Отличный канонический пример для демонстрации — электронная таблица Excel.
Если ввести цифру 2 в первую ячейку, а цифру 3 во вторую, а затем, с помощью встроенной в Excel функции SUM, запросить их сумму — таблица её рассчитает. Ничего неожиданного. Но если изменить число в первой ячейке, то сумма обновится автоматически.
Обычно JavaScript так не работает. Если попробовать реализовать похожее на JavaScript, то это могло бы выглядеть так:
let val1 = 2
let val2 = 3
let sum = val1 + val2
console.log(sum) // 5
val1 = 3
console.log(sum) // по-прежнему 5
2
3
4
5
6
7
8
9
Но если изменить первое число, то сумма не пересчитается с его учётом.
Как же этого добиться на JavaScript?
С высоты птичьего полёта, должна быть возможность сделать несколько вещей:
- Отслеживать когда значение считывается. Например, для выражения
val1 + val2
будет считываться значениеval1
иval2
. - Определять когда значение изменяется. Например, при присвоении
val1 = 3
. - Перезапускать код, который считывал значения изначально. Например, снова выполнить
sum = val1 + val2
для обновления значенияsum
.
Используя код предыдущего примера, нет никакой возможности сделать это. Вернёмся к нему чуть позже, чтобы разобрать как можно адаптировать его для совместимости с системой реактивности Vue.
Сначала разберёмся как Vue реализует перечисленные выше требования к реактивности.
# Как Vue определяет какой код выполнялся
Чтобы получить возможность пересчитывать сумму всякий раз, когда значения изменятся, первое что потребуется — обернуть этот код в функцию:
const updateSum = () => {
sum = val1 + val2
}
2
3
Но каким образом расскажем Vue об этой функции?
Vue с помощью эффекта отслеживает какая функция в данный момент была запущена. Эффект — обёртка вокруг функции, которая начинает отслеживание непосредственно перед её вызовом. Таким образом Vue знает, какой эффект выполняется в любой заданной точке, и может при необходимости запустить его снова.
Чтобы лучше это понять, давайте попробуем реализовать нечто подобное самостоятельно, без Vue, и посмотреть как это может работать.
Что для начала требуется, так это что-то, что может обернуть вычисление суммы вот так:
createEffect(() => {
sum = val1 + val2
})
2
3
Для этого требуется создать createEffect
, чтобы отслеживать выполнение функции вычисления суммы. Реализовать подобное можно примерно так:
// Стек для хранения запущенных эффектов
const runningEffects = []
const createEffect = fn => {
// Оборачиваем переданную fn в функцию эффекта
const effect = () => {
runningEffects.push(effect)
fn()
runningEffects.pop()
}
// Автоматически сразу запускаем эффект
effect()
}
2
3
4
5
6
7
8
9
10
11
12
13
14
Запуск эффекта будет добавлять его в массив runningEffects
перед вызовом fn
. Теперь, если потребуется узнать какой эффект запущен в данный момент, достаточно проверить этот массив.
Эффекты выступают в качестве стартовой точки для многих возможностей. Например, как отрисовка компонента, так и вычисляемые свойства внутри используют эффекты. Каждый раз, когда что-то «волшебным» образом реагирует на изменение данных, можно быть уверенным что это было обёрнуто в эффект.
Несмотря на то, что публичный API Vue не позволяет создавать эффекты напрямую, он предоставляет доступ к функции watchEffect
, которая ведёт себя очень похоже на createEffect
из примера выше. Подробнее это обсудим далее в руководстве.
Но понимание какой код выполняется — лишь одна из частей головоломки. Как же Vue узнаёт какие значения использует эффект и как определяет что они изменились?
# Как Vue отслеживает изменения
Отследить переназначение локальных переменных в предыдущих примерах не получится, такого механизма просто нет в JavaScript. Можно лишь отслеживать изменения свойств объектов.
Поэтому, когда возвращается простой объект JavaScript из функции data
компонента, Vue обернёт его в Proxy (opens new window) с обработчиками для get
и set
. Прокси были представлены в ES6 и позволяют Vue 3 избавиться от ограничений системы реактивности, которые существовали в предыдущих версиях Vue.
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, property) {
console.log('перехвачен!')
return target[property]
}
}
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 есть одна сложность — привязка к this
. Хочется, чтобы любой метод был привязан к Proxy, а не к отслеживаемому объекту, чтобы была возможность перехватывать и их тоже. К счастью, ES6 предоставляет ещё одну возможность, названную Reflect
, которая позволяет избежать этой проблемы с минимальными усилиями:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, 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
Первым шагом на пути реализации реактивности с Proxy будет отслеживание считывания свойства. Это можно сделать в обработчике, в функции с названием track
, в которую передаётся target
и property
:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
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
Реализация метода track
здесь не показана. Она проверяет, какой эффект в данный момент запущен, и записывает его вместе с target
и property
. Таким образом Vue определяет, что свойство является зависимостью эффекта.
Наконец, необходимо повторно запустить эффект при изменении значения свойства. Для этого воспользуемся обработчиком set
в прокси:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
trigger(target, property)
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 реализуются эти шаги:
- Отслеживать когда значение считывается: функция
track
в обработчикеget
прокси записывает свойство и текущий эффект. - Определять когда значение изменяется: в прокси вызывается обработчик
set
. - Перезапускать код, который считывал значения изначально: функция
trigger
ищет какие эффекты зависят от изменившегося свойства и запускает их.
Проксируемый объект невидим для пользователя, но под капотом он позволяет Vue отслеживать зависимости и уведомлять о считывании свойств или их изменениях. Небольшое отличие лишь в том, что проксированные объекты при выводе в консоль форматируются немного иначе, поэтому рекомендуем установить vue-devtools (opens new window), чтобы отслеживать их состояние в более удобном интерфейсе.
Если переписать оригинальный пример с использованием компонента, то получится так:
const vm = createApp({
data() {
return {
val1: 2,
val2: 3
}
},
computed: {
sum() {
return this.val1 + this.val2
}
}
}).mount('#app')
console.log(vm.sum) // 5
vm.val1 = 3
console.log(vm.sum) // 6
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
Объект, возвращаемый из data
, будет обёрнут в реактивный прокси и сохранён как this.$data
. Свойства this.val1
и this.val2
будут псевдонимами для this.$data.val1
и this.$data.val2
соответственно, поэтому они проходят через один и тот же прокси.
Vue обернёт функцию для sum
в эффект. При попытке чтения this.sum
, он запустит этот эффект для вычисления значения. Реактивный прокси вокруг $data
позволит отследить, что свойства val1
и val2
были прочитаны во время выполнения этого эффекта.
Начиная с Vue 3, реактивность теперь доступна как отдельный пакет (opens new window). Функция, которая оборачивает $data
в прокси называется reactive
. Её можно вызвать напрямую, если нужно обернуть объект в реактивный прокси без необходимости использовать компонент:
const proxy = reactive({
val1: 2,
val2: 3
})
2
3
4
С возможностями, предоставляемыми пакетом реактивности, познакомимся подробнее в следующих разделах. Там будут рассмотрены функции reactive
и watchEffect
, которые уже повстречались, а также способы использования других функций реактивности, таких как computed
и watch
, без необходимости создания компонента.
# Проксированные объекты
Под капотом Vue отслеживает все объекты, которые были сделаны реактивными, поэтому для каждого объекта всегда возвращается свой Proxy.
Когда требуется доступ к вложенному объекту внутри реактивной Proxy, этот объект перед возвращением также преобразуется в Proxy.
const handler = {
get(target, property, receiver) {
track(target, property)
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
13
# Proxy vs оригинальная сущность
При использовании Proxy нужно запомнить — проксируемый объект не будет равен оригинальному объекту в строгих сравнениях (===
). Например:
const obj = {}
const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
2
3
4
Также будут затронуты и другие операции, основанные на строгом сравнении, например .includes()
или .indexOf()
.
Хорошей практикой будет никогда не сохранять ссылку на оригинальный объект и работать только с реактивной версией.
const obj = reactive({
count: 0
}) // никакой ссылки на оригинал
2
3
В таком случае можно гарантировать что поведение при сравнении и реактивность будут вести себя так, как и ожидается.
Обратите внимание, что Vue не оборачивает в Proxy примитивные значения (такие как числа или строки), поэтому ===
можно использовать напрямую с такими значениями:
const obj = reactive({
count: 0
})
console.log(obj.count === 0) // true
2
3
4
5
# Как отрисовка реагирует на изменения
Шаблон компонента компилируется в render
-функцию. Функция render
создаёт дерево из VNode, которое описывает как компонент должен быть отрисован. Она также обёрнута в эффект, что позволяет Vue отслеживать свойства, которые считываются во время работы.
Функция render
концептуально очень похожа на свойство computed
. Vue не отслеживает каким именно образом зависимости используются, он только знает, что они использовались в какой-то момент во время работы функции. Если какое-либо из этих свойств изменится впоследствии, то это вызовет повторный запуск эффекта и перезапуск функции render
для генерации нового дерева из VNode. После чего оно будет использовано для внесения необходимых изменений в DOM.
See the Pen Второе объяснение реактивности с прокси во Vue 3 by Vue (@Vue) on CodePen.
При использовании Vue 2.x или более ранних версий, есть дополнительные особенности отслеживания изменений, которые подробнее рассмотрены здесь.