# Особенности отслеживания изменений во 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` НЕ РЕАКТИВНО
2
3
4
5
6
7
8
9
Во Vue в уже существующий экземпляр нельзя динамически добавлять новые корневые реактивные свойства. Но можно добавить реактивное свойство во вложенный объект с помощью метода Vue.set(object, propertyName, value)
:
Vue.set(vm.someObject, 'b', 2)
Или можно использовать метод экземпляра vm.$set
— псевдоним глобального Vue.set
:
this.$set(this.someObject, 'b', 2)
Иногда нужно добавить несколько свойств в существующий объект, например, с помощью Object.assign()
или _.extend()
. Если так поступить, то добавленные свойства не станут реактивными. Нужно создавать новый объект, который будет содержать как поля оригинального объекта, так и поля объекта с добавляемыми свойствами:
// вместо `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
2
# Для массивов
Vue не может обнаруживать следующие изменения в массивах:
- Прямую установку элемента по индексу:
vm.items[indexOfItem] = newValue
- Явное изменение длины массива:
vm.items.length = newLength
Например:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // НЕ РЕАКТИВНО
vm.items.length = 2 // НЕ РЕАКТИВНО
2
3
4
5
6
7
8
Первую проблему можно решить двумя способами (в обоих случаях эффект аналогичен vm.items[indexOfItem] = newValue
и запустят обновление состояния в системе реактивности):
// Использовать Vue.set
Vue.set(vm.items, indexOfItem, newValue)
2
// Использовать Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
2
Можно использовать метод экземпляра vm.$set
(opens new window) — псевдоним глобального Vue.set
:
vm.$set(vm.items, indexOfItem, newValue)
Вторую проблему можно решить с помощью splice
:
vm.items.splice(newLength)
# Объявление реактивных свойств
Так как Vue не позволяет динамически добавлять корневые реактивные свойства, то все корневые поля необходимо инициализировать в экземплярах компонента изначально, хотя бы пустыми значениями:
var vm = new Vue({
data: {
// объявляем свойство message с пустой строкой
message: ''
},
template: '<div>{{ message }}</div>'
})
// когда-то позднее задаём значение `message`
vm.message = 'Привет!'
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>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'новое сообщение' // изменяем данные
vm.$el.textContent === 'новое сообщение' // false
Vue.nextTick(function() {
vm.$el.textContent === 'новое сообщение' // true
})
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) // => 'обновлено'
})
}
}
})
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) // => 'обновлено'
}
}
2
3
4
5
6
7
8