Vue最独特的特性之一,是其非侵入性的响应式系统,数据模型仅仅是普通的JavaScript对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单。接下来就讲讲Vue响应式系统的原理。
1、如何追踪变化
当一个Vue实例创建时,Vue会遍历data选项(你传入的普通JavaScript对象)中的所有属性,并使用Object.defineProperty把它们转化为getter/setter,并在内部追踪相关依赖,在属性被访问和修改时通知变更。
每个组件实例都对应一个watcher实例,它会在组件渲染的过程中将“解触”过的数据属性记录为依赖。当之后依赖项的setter触发时,会通知watcher,从而使它关联的组件重新渲染。
2、检测变化的注意事项
受现代JavaScript的限制(而且Object.observe也已经废弃),Vue无法检测到对象属性的添加或删除。由于Vue会在初始化实例时对属性执行getter/setter转化,所以属性必须在data对象上存在才能让Vue将它转换为响应式的。例如:
var vm = new Vue({
data:{
a:1,
userProfile:{
name:‘Alice’,
},
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
对于已经创建的实例,Vue不允许动态添加根级别的响应式属性。但是可以使用Vue.set(object,propertyName,value)
方法向嵌套对象添加响应式属性。例如:
Vue.set(vm.userProfile,'age',6);
还可以使用vm.$set
实例方法,这也是全局Vue.set方法的别名,例如:
this.$set(this.userProfile,'age',6);
有时你可能需要为已有的对象赋值多个新属性,比如使用Object.assign()
或_.extend()
。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新对象。例如:
this.userProfile = Object.assign({},this.userProfile,{
'age':6,
'job':'student'
});
3、异步更新队列
Vue在更新DOM时是异步执行的。只要侦听到数据变化,Vue将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个watcher被多次触发,只会被推入到队列中一次。这种在缓冲事去除重复数据对于避免不必要的计算和DOM操作非常重要。然后,在下一个的事件循环“tick”中,Vue刷新队列并执行实际(已去重的)工作。Vue在内部对异步队列尝试使用原生的Promise.then
、MutationObserver
和 setImmediate
,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
例如,当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 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: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
}
})