# Подробнее о реактивности

Пришло время разобраться в теме поподробнее! Одной из отличительных особенностей 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
1
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 легче и производительнее.

Это достаточно поверхностное объяснение, которое требует некоторых знаний о 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
1
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
1
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
1
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
1
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
1
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
    }
  }
  // ...
}
1
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
1
2
3
4

Оригинальный объект и обёрнутая в Proxy версия в большинстве случаев будут вести себя одинаково, но могут выдать неправильный результат в операциях со строгим сравнением, такими как .filter() или .map(). Эта особенность вряд ли возникнет при использовании Options API, потому что все свойства реактивного состояния вызываются из this и уже гарантированно будут Proxy.

Однако, при использовании Composition API для явного создания реактивных объектов, хорошей практикой будет никогда не сохранять ссылку на оригинальный объект и работать только с реактивной версией.

const obj = reactive({
  count: 0
}) // не ссылается на оригинал
1
2
3

# Наблюдатели

У каждого экземпляра компонента есть соответствующий экземпляр наблюдателя, который регистрирует любые свойства, «затронутые» во время отрисовки компонента в качестве зависимостей. Позже, при срабатывании сеттера зависимости, он уведомит наблюдателя, что приведёт к перерисовке компонента.

При передаче объекта данных в экземпляр компонента в качестве локальных данных, Vue преобразует его в Proxy. Этот Proxy позволит Vue отслеживать зависимости и получать уведомления об их изменениях. Каждое свойство рассматривается как зависимость.

После первой отрисовки у компонента будет список отслеживаемых зависимостей, полученный из свойств, затронутых в момент отрисовки. И наоборот, компонент подписывается на каждое из этих свойств. Когда Proxy перехватывает операцию обновления, все подписанные на свойство компоненты будут уведомлены и перерисованы.

При использовании Vue 2.x или более ранних версий, есть дополнительные особенности отслеживания изменений, которые подробнее рассмотрены здесь.

Deployed on Netlify.
Последнее обновление: 2021-03-06, 19:47:51 UTC