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

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

Вследствие ограничений JavaScript есть изменения, которые Vue не может обнаружить. Однако существуют способы обойти эту проблему, чтобы сохранить реактивность.

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

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

Решить первую проблему можно любым из двух способов, оба дают эффект аналогичный 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

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

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

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

На всякий случай напомним, что во Vue обновление DOM выполняется асинхронно. Каждый раз, когда обнаруживается изменение в данных, создаётся очередь, которая используется в качестве буфера для этого и последующих изменений, происходящих в текущей итерации ("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

Существует также метод экземпляра 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

Deployed on Netlify.
Последнее обновление: 2020-10-31, 21:23:54 UTC