深入响应式

我们已经涵盖了大多数基础知识 - 现在是深入研究的时候了!Vue.js 最独特的特性之一是其不显眼的响应式系统 - 模型只是普通的 JavaScript 对象,修改它,视图就会更新。这使得状态管理非常简单直观,但了解其工作原理也很重要,以避免一些常见的错误。在本节中,我们将深入探讨 Vue.js 响应式系统的底层细节。

如何跟踪更改

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

getter/setter 对用户来说是不可见的,但在幕后,它们使 Vue.js 能够在访问或修改属性时执行依赖项跟踪和更改通知。需要注意的是,当转换后的数据对象被记录时,浏览器控制台会以不同的方式格式化 getter/setter,因此请确保使用 vm.$log() 实例方法以获得更易于检查的输出。

对于模板中的每个指令/数据绑定,都会有一个相应的 watcher 对象,它会记录在评估过程中“触碰”的任何属性作为依赖项。稍后,当依赖项的 setter 被调用时,它会触发 watcher 重新评估,进而导致其关联的指令执行 DOM 更新。

data

更改检测注意事项

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

var data = { a: 1 }
var vm = new Vue({
data: data
})
// `vm.a` and `data.a` are now reactive
vm.b = 2
// `vm.b` is NOT reactive
data.b = 2
// `data.b` is NOT reactive

但是,有一些方法可以在实例创建后 添加属性并使其具有响应性

对于 Vue 实例,您可以使用 $set(path, value) 实例方法

vm.$set('b', 2)
// `vm.b` and `data.b` are now reactive

对于普通数据对象,您可以使用全局 Vue.set(object, key, value) 方法

Vue.set(data, 'c', 3)
// `vm.c` and `data.c` are now reactive

有时您可能希望将多个属性分配到现有对象上,例如使用 Object.assign()_.extend()。但是,添加到对象的新属性不会触发更改。在这种情况下,请创建一个新的对象,其中包含原始对象和混合对象中的属性

// instead of `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

还有一些与数组相关的注意事项,这些注意事项在 列表渲染部分的前面已经讨论过

初始化您的数据

虽然 Vue.js 提供了 API 来动态添加响应式属性,但建议在 data 选项中预先声明所有响应式属性。

不要这样做

var vm = new Vue({
template: '<div>{{msg}}</div>'
})
// add `msg` later
vm.$set('msg', 'Hello!')

而是这样做

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

这种模式背后的原因有两个

  1. data 对象就像您组件状态的模式。预先声明所有响应式属性使组件代码更易于理解和推理。

  2. 在 Vue 实例上添加顶级响应式属性将强制其作用域内的所有 watcher 重新评估,因为它之前不存在,并且没有 watcher 可以将其作为依赖项进行跟踪。性能通常是可以接受的(本质上与 Angular 的脏检查相同),但当您正确初始化数据时可以避免。

异步更新队列

默认情况下,Vue.js 异步执行 DOM 更新。每当观察到数据更改时,Vue 将打开一个队列并缓冲在同一个事件循环中发生的任何数据更改。如果同一个 watcher 被触发多次,它只会被推入队列一次。然后,在下一个事件循环“tick”中,Vue 会刷新队列,只执行必要的 DOM 更新。在内部,Vue 使用 MutationObserver(如果可用)进行异步排队,并回退到 setTimeout(fn, 0)

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

<div id="example">{{msg}}</div>
var vm = new Vue({
el: '#example',
data: {
msg: '123'
}
})
vm.msg = '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>{{msg}}</span>',
data: function () {
return {
msg: 'not updated'
}
},
methods: {
updateMessage: function () {
this.msg = 'updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function () {
console.log(this.$el.textContent) // => 'updated'
})
}
}
})

计算属性内部

需要注意的是,Vue.js 计算属性 不是简单的 getter。每个计算属性都会跟踪自己的响应式依赖项。当计算属性被评估时,Vue.js 会更新其依赖项列表并缓存结果值。只有当跟踪到的依赖项之一发生更改时,缓存的值才会失效。因此,只要依赖项没有更改,访问计算属性将直接返回缓存的值,而不是调用 getter。

为什么要缓存?假设我们有一个昂贵的计算属性 A,它需要遍历一个巨大的数组并进行大量计算。然后,我们可能还有其他计算属性依赖于 A。如果没有缓存,我们将比必要时更多地调用 A 的 getter!

由于计算属性缓存,当您访问计算属性时,getter 函数并不总是被调用。考虑以下示例

var vm = new Vue({
data: {
msg: 'hi'
},
computed: {
example: function () {
return Date.now() + this.msg
}
}
})

计算属性 example 只有一个依赖项:vm.msgDate.now() 不是响应式依赖项,因为它与 Vue 的数据观察系统无关。因此,当您以编程方式访问 vm.example 时,您会发现时间戳保持不变,除非 vm.msg 触发重新评估。

在某些用例中,您可能希望保留简单的 getter 类行为,即每次访问 vm.example 时,它都会简单地再次调用 getter。您可以通过关闭特定计算属性的缓存来实现这一点

computed: {
example: {
cache: false,
get: function () {
return Date.now() + this.msg
}
}
}

现在,每次访问 vm.example 时,时间戳都将是最新的。但是,请注意,这只会影响 JavaScript 中的编程访问;数据绑定仍然是依赖驱动的。当您在模板中将计算属性绑定到 {{example}} 时,只有当响应式依赖项发生更改时,DOM 才会更新。