Close
升级到 Vue 3 | Vue 2 生命周期结束

深入响应式

现在是深入了解的时候了!Vue 最独特的特性之一是其非侵入式的响应式系统。模型只是普通的 JavaScript 对象。当你修改它们时,视图会更新。这使得状态管理变得简单直观,但了解其工作原理也很重要,可以避免一些常见的陷阱。在本节中,我们将深入探讨 Vue 响应式系统的底层细节。

如何跟踪更改

当你将一个普通的 JavaScript 对象作为 data 选项传递给 Vue 实例时,Vue 会遍历其所有属性,并使用 getter/setter 将它们转换为 Object.defineProperty。这是一个 ES5 独有的不可模拟的功能,这就是为什么 Vue 不支持 IE8 及以下版本的原因。

getter/setter 对用户是不可见的,但在幕后,它们使 Vue 能够在访问或修改属性时执行依赖项跟踪和更改通知。需要注意的是,当转换后的数据对象被记录时,浏览器控制台会以不同的方式格式化 getter/setter,因此你可能需要安装 vue-devtools 来获得更友好的检查界面。

每个组件实例都有一个对应的 **观察者** 实例,它会记录组件渲染期间“触碰”的任何属性作为依赖项。稍后,当依赖项的 setter 被触发时,它会通知观察者,观察者反过来会使组件重新渲染。

Reactivity Cycle

更改检测注意事项

由于 JavaScript 的限制,Vue **无法检测**某些类型的更改。但是,有一些方法可以规避它们以保持响应性。

对于对象

Vue 无法检测属性添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此属性必须存在于 data 对象中,才能使 Vue 转换它并使其具有响应性。例如

var vm = new Vue({
data: {
a: 1
}
})
// `vm.a` is now reactive

vm.b = 2
// `vm.b` is NOT reactive

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()。但是,添加到对象的新属性不会触发更改。在这种情况下,创建一个新的对象,其中包含原始对象和混合对象中的属性

// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 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' // is NOT reactive
vm.items.length = 2 // is NOT reactive

为了克服第一个问题,以下两种方法都将实现与 vm.items[indexOfItem] = newValue 相同的效果,但也会在响应式系统中触发状态更新

// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)

你也可以使用 vm.$set 实例方法,它是全局 Vue.set 的别名

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

为了解决第二个问题,可以使用 splice

vm.items.splice(newLength)

声明响应式属性

由于 Vue 不允许动态添加顶级响应式属性,因此你必须通过预先声明所有顶级响应式数据属性来初始化 Vue 实例,即使是空值

var vm = new Vue({
data: {
// declare message with an empty value
message: ''
},
template: '<div>{{ message }}</div>'
})
// set `message` later
vm.message = 'Hello!'

如果你没有在 data 选项中声明 message,Vue 会警告你渲染函数正在尝试访问不存在的属性。

这种限制背后有技术原因 - 它消除了依赖项跟踪系统中的一类边缘情况,也使 Vue 实例与类型检查系统更加兼容。但从代码可维护性的角度来看,还有一个重要的考虑因素:data 对象就像你组件状态的模式。预先声明所有响应式属性,使组件代码在稍后重新访问或被其他开发人员阅读时更容易理解。

异步更新队列

如果你还没有注意到,Vue 是 **异步** 执行 DOM 更新的。每当观察到数据更改时,它会打开一个队列,并缓冲在同一个事件循环中发生的任何数据更改。如果同一个观察者被多次触发,它只会被推入队列一次。这种缓冲去重对于避免不必要的计算和 DOM 操作非常重要。然后,在下一个事件循环“tick”中,Vue 会刷新队列,并执行实际的(已去重的)工作。在内部,Vue 尝试使用原生 Promise.thenMutationObserversetImmediate 来进行异步排队,并回退到 setTimeout(fn, 0)

例如,当你设置 vm.someData = 'new value' 时,组件不会立即重新渲染。它将在下一个“tick”中更新,此时队列被刷新。大多数情况下我们不需要关心这一点,但当你想做一些依赖于更新后 DOM 状态的事情时,它可能会很棘手。虽然 Vue.js 通常鼓励开发人员以“数据驱动”的方式思考,并避免直接触碰 DOM,但有时可能需要动手操作。为了等待 Vue.js 在数据更改后完成 DOM 更新,你可以在数据更改后立即使用 Vue.nextTick(callback)。回调将在 DOM 更新后被调用。例如

<div id="example">{{ message }}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // change data
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})

还有一个 vm.$nextTick() 实例方法,它在组件内部特别方便,因为它不需要全局 Vue,并且其回调的 this 上下文将自动绑定到当前 Vue 实例

Vue.component('example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: 'not updated'
}
},
methods: {
updateMessage: function () {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function () {
console.log(this.$el.textContent) // => 'updated'
})
}
}
})

由于 $nextTick() 返回一个 Promise,你可以使用新的 ES2017 async/await 语法来实现与上述相同的效果

methods: {
updateMessage: async function () {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
await this.$nextTick()
console.log(this.$el.textContent) // => 'updated'
}
}