# Особенности отслеживания изменений во Vue 2

Этот раздел относится только к Vue версий 2.x и ниже и предполагает, что уже изучили и разобрались с разделом подробнее о реактивности. Если нет — прочитайте его сначала.

Есть изменения, которые Vue не может обнаружить, из-за ограничений JavaScript. Но есть способы решения этой проблемы, чтобы сохранить реактивность.

# Для объектов

Vue не может обнаруживать добавление или удаление свойства. Поскольку Vue добавляет геттер/сеттер на этапе инициализации экземпляра, то свойство должно присутствовать в объекте data чтобы Vue преобразовал его и сделал реактивным. Например:

var vm = new Vue({
  data: {
    a: 1
  }
})
// `vm.a` теперь реактивное свойство

vm.b = 2
// `vm.b` НЕ РЕАКТИВНО
1
2
3
4
5
6
7
8
9

Во Vue в уже существующий экземпляр нельзя динамически добавлять новые корневые реактивные свойства. Но можно добавить реактивное свойство во вложенный объект с помощью метода Vue.set(object, propertyName, value):

Vue.set(vm.someObject, 'b', 2)
1

Или можно использовать метод экземпляра vm.$set — псевдоним глобального Vue.set:

this.$set(this.someObject, 'b', 2)
1

Иногда нужно добавить несколько свойств в существующий объект, например, с помощью Object.assign() или _.extend(). Если так поступить, то добавленные свойства не станут реактивными. Нужно создавать новый объект, который будет содержать как поля оригинального объекта, так и поля объекта с добавляемыми свойствами:

// вместо `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
1
2

# Для массивов

Vue не может обнаруживать следующие изменения в массивах:

  1. Прямую установку элемента по индексу: vm.items[indexOfItem] = newValue
  2. Явное изменение длины массива: vm.items.length = newLength

Например:

var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})

vm.items[1] = 'x' // НЕ РЕАКТИВНО
vm.items.length = 2 // НЕ РЕАКТИВНО
1
2
3
4
5
6
7
8

Первую проблему можно решить двумя способами (в обоих случаях эффект аналогичен vm.items[indexOfItem] = newValue и запустят обновление состояния в системе реактивности):

// Использовать Vue.set
Vue.set(vm.items, indexOfItem, newValue)
1
2
// Использовать Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
1
2

Можно использовать метод экземпляра vm.$set (opens new window) — псевдоним глобального Vue.set:

vm.$set(vm.items, indexOfItem, newValue)
1

Вторую проблему можно решить с помощью splice:

vm.items.splice(newLength)
1

# Объявление реактивных свойств

Так как Vue не позволяет динамически добавлять корневые реактивные свойства, то все корневые поля необходимо инициализировать в экземплярах компонента изначально, хотя бы пустыми значениями:

var vm = new Vue({
  data: {
    // объявляем свойство message с пустой строкой
    message: ''
  },
  template: '<div>{{ message }}</div>'
})

// когда-то позднее задаём значение `message`
vm.message = 'Привет!'
1
2
3
4
5
6
7
8
9
10

Если не объявить поле message в опции data, то Vue выведет предупреждение, что функция отрисовки пытается получить доступ к несуществующему свойству.

Есть технические причины для этого ограничения: оно позволяет исключить целый класс граничных случаев в системе учёта зависимостей, а также упростить взаимодействие компонента с системами проверки типов. Но что гораздо важнее, с этим ограничением становится проще поддерживать код, так как объект data теперь можно рассматривать как схему состояния компонента. Код, в котором все реактивные свойства компонента перечисляются заранее, намного проще для понимания и поддержки.

# Асинхронная очередь обновлений

Напомним, что обновление DOM во Vue выполняется асинхронно. Каждый раз, при обнаружении изменения в данных, создаётся очередь, которая используется в качестве буфера для этого и последующих изменений, происходящих в текущей итерации цикла событий («tick»). Даже если один и тот же наблюдатель сработает несколько раз, в очередь всё равно он попадёт лишь один раз. Использование буфера и устранение дублирования позволяют свести к минимуму вычисления и манипуляции с DOM. В следующей итерации цикла событий Vue разбирает очередь и выполняет актуальные обновления (уже без повторений). Для асинхронной постановки задач в очередь на низком уровне используются Promise.then, MutationObserver и setImmediate, а если недоступны setTimeout(fn, 0).

Итак, если выполнить код vm.someData = 'новое значение', компонент не будет сразу же отрисован. Он обновится в следующей итерации при разборе очереди. Эту особенность чаще всего можно не принимать в расчёт, но иногда требуется дождаться состояния в которое после обновления данных перейдёт DOM. Хотя манипулировать DOM напрямую нежелательно, а систему в целом предпочтительнее проектировать чтобы в ней были первичные данные, иногда этого не избежать. Чтобы выполнить какой-нибудь код только после завершения обновления DOM, можно использовать Vue.nextTick(callback) сразу после изменения данных. Коллбэк будет вызван после обновления DOM. Например:

<div id="example">{{ message }}</div>
1
var vm = new Vue({
  el: '#example',
  data: {
    message: '123'
  }
})

vm.message = 'новое сообщение' // изменяем данные
vm.$el.textContent === 'новое сообщение' // false
Vue.nextTick(function() {
  vm.$el.textContent === 'новое сообщение' // true
})
1
2
3
4
5
6
7
8
9
10
11
12

Есть также метод экземпляра vm.$nextTick(), который удобен для использования внутри компонентов, потому что не требует обращения к глобальной переменной Vue, а также автоматически связывает контекст this коллбэка с текущим экземпляром компонента:

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function() {
    return {
      message: 'не обновлено'
    }
  },
  methods: {
    updateMessage() {
      this.message = 'обновлено'
      console.log(this.$el.textContent) // => 'не обновлено'
      this.$nextTick(function() {
        console.log(this.$el.textContent) // => 'обновлено'
      })
    }
  }
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

Так как $nextTick() возвращает Promise, то можно использовать синтаксис async/await из ES2017 (opens new window):

 methods: {
   updateMessage: async function () {
     this.message = 'обновлено'
     console.log(this.$el.textContent) // => 'не обновлено'
     await this.$nextTick()
     console.log(this.$el.textContent) // => 'обновлено'
   }
 }
1
2
3
4
5
6
7
8