主要内容:
- 改变是如何被追踪到的
- 改变检测的一些说明及声明属性的方式
- 声明响应式属性的形式及原因
- 异步更新队列的方式及如何在nextTick下手动更新数据
现在是时候深入研究一下了!Vue最独特的功能之一就是非侵入式的响应系统。模型仅仅是纯JavaScript对象。当你修改他们,视图更新。它使状态管理更加简单直观,然而理解他们是如何工作的以避免一些常见的陷阱。在这章,我们将深入研究Vue的响应系统的一些细节。
如何追踪变化
当你把普通的JavaScript对象传递给一个Vue实例作为data选项,Vue将遍历所有的属性,并使用Object.defineProperty
将这些属性转为getter/setters
。这是ES5中不可模拟的特性,这就是为什么Vue不支持IE8及以下的版本。
getter/setter是对用户不可见的,当属性被访问或修改的时候,钩子会被启用,使得Vue执行依赖追踪和改变通知。需要注意的是,在打印转换数据对象时,浏览器控制台打印的getters/setters格式并不相同。所以为了更友好的界面检查你可能需要安装vue-devtools。
每一个组件实例有一个一致的watcher实例,它会把组件渲染的过程中把任何属性记录为依赖。而后当一个依赖的setter被触发的时候,它会通知watcher,反过来导致组件被重新渲染。
变化检测说明
由于现代Javascript的限制(而且Object.observe
已经被终止),Vue不能检测到属性的添加或删除。由于Vue在实例初始化期间执行了getter/setter转换过程,为了Vue转换换它和响应它,属性必须在data对象中存在。例如:
var vm = new Vue({
data: {
a: 1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 不是响应式的
Vue不允许动态的添加一个新的属性到一个已经被创建好的实例中。然而,使用Vue.set(object, key, 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 = Objet.assign({}, this.someObject, {a: 1, b: 2})
这里有一些和属性相关的注意事项,在之前的列表渲染章节已经讲过。
声明响应式属性
由于Vue不允许动态添加根级响应属性,你必须通过声明所有根级响应式属性初始化Vue实例,即使值为空:
var vm = new Vue({
data: {
// 用一个空值声明message
message: ''
},
template: '<div>{{ message }}/div>'
})
// 稍后设置'message'
vm.message = 'Hello!'
如果你没有声明message在data操作项,渲染函数试着访问一个不存在的属性,Vue将警告你。
这样的限制在背后是有技术上的原因的-它消除了依赖追踪中的边缘情况,也使得Vue实例在类型检查系统的帮助下发挥更好的作用。而且在代码可维护性方法也有重要的考虑:data
对象就像组件状态的概要。当一会再看或者另一个开发者读代码的时候,预先声明所有响应式属性使得组件代码更容易理解。
异步更新队列
可能你没有注意到,Vue执行DOM更新是异步的
。无论何时当一个数据改变被观察到,它会开一个队列,缓存发生在同一个消息循环内的所有data改变。如果相同的watcher被触发多次,它仅仅被pushed到队列一次。这种缓存重复数据删除对于避免不必要的计算和DOM维护是重要的。然后,在下一次消息循环"tick"中,Vue刷新队列并执行实际的(已经去重的)工作。内部的Vue尝试对异步队列使用原生的Promise.then
和MessageChannel
,如果环境不支持,会采用setTimeout(fn, 0)
代替。
例如,当你设置vm.someData = 'new value'
,组件将不会立即重新渲染。它将更新在下一个’tick’ , 当一个队列被刷新后。多数情况下你不需要关心这个,但是当你想在DOM状态更新之后做点什么。这就可能有些棘手。虽然Vue一般鼓励开发者沿着数据驱动的方式思考,避免直接接触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' // 改变数据
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对象,所以你可以使用新的ES2016 async/wait语法完成相同的事情:
methods: {
async updateMessage: function () {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
await this.$nextTick()
console.log(this.$el.textContent) // => 'updated'
}
}