Vue中的model对象被整合到了Vue实例vm的data属性中,数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。所以本文就data的更新原理做一个解析。详情见Vue官方文档
一、 
当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。
那么什么是getter/setter呢,简单代码说明一下
var o = {
a: 7,
get b() {
return this.a + 1;
},
set b(x) {
this.a = x / 2
}
};
我们为o中的函数b设置一个set和一个get,这样会得到以下这个对象o
可以看到b这时候是一个不确定的状态,这是因为并不存在一个叫b的属性,我们要通过get b()或者get b()才能计算出他的值,点击一下他就会成为8
这时候我们给b赋值
o.b=20
//其实相当于这样o.b(20)
这时候a的值会随之改变,这时候被赋值的内容会被当做参数传入set b(x)
总结一下:b是一个定义的函数,但是在前面加上get和set以后,我们在调用这个函数的时候不用加()
使用Object.defineProperties的方法,同样也可以对一个已创建的对象在任何时候为其添加getter或setter方法。
第一个参数是你想定义getter或setter方法的对象
第二个参数是一个对象,这个对象的属性名用作getter或setter的名字,属性名对应的属性值用作定义getter或setter方法的函数
Object.defineProperties(o, {
"b": { get: function () { return this.a + 1; } },
"c": { set: function (x) { this.a = x / 2; } }
});
了解了上面的getter/setter以后我们还是不知道Vue是如何知道data变化的,所以下面介绍下Vue是如何监听data的
二、 
我们先来看下如何利用setter来对data的变化做一个限制
let data1 = {}
Object.defineProperty(data1, 'n', {
value: 0
})
简单的给一个对象赋值,注意value
let data2 = {}
data2._n = 0 // _n 用来偷偷存储 n 的值
Object.defineProperty(data2, 'n', {
get(){
return this._n
},
set(value){
if(value < 0) return
this._n = value
}
})
data2.n = -1
console.log(`${data2.n} 设置为 -1 失败`)
//n=0
在set中对传入的n做一个限制,如果它小于0,则不赋值给_n这个临时变量
这里为什么要使用一个临时变量才存储n的值呢,因为如果直接将return n那么则会递归的去调用get()导致爆栈
但是如果我们直接改变data2._n的值呢,这时候set就不能起到过滤的作用了,因为我们的临时变量是完全暴露在外的
data2._n = -1
console.log(`${data2.n} 设置为 -1 失败`)
//n=-1
所以接下来我们想到了使用代理,将临时变量给隐藏起来,定义一个函数proxy
let data3 = proxy({ data:{n:0} })
// 括号里是匿名对象,无法访问
function proxy({data}/* 解构赋值 */){
const obj = {}
// 这里的 'n' 写死了,理论上应该遍历 data 的所有 key,这里做了简化
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj // obj 就是代理
}
data3.n = -1
console.log(`${data3.},设置为 -1 失败`)
//n=0
此时我们使用的是一个匿名对象,所以无法对其属性进行修改
但是如果我们给这个匿名对象是一个引用呢
let myData = {n:0}
let data4 = proxy({ data:myData }) // 括号里是匿名对象,无法访问
myData.n = -1
//这里是直接给data中的n赋值了,他是通过get使得n改变的
//而上一次data3.n是通过obj的set赋值给n的所以被set过滤了
console.log(`${data4.n},设置为 -1 失败了吗!?`)
//n=-1
既然你想改掉我的源对象中的n,那么好,我把源对象的data变化设置一个监听
let myData5 = {n:0}
let data5 = proxy2({ data:myData5 }) // 括号里是匿名对象,无法访问
function proxy2({data}){
let value = data.n
//将原来的n赋值到value这里,然后把源对象n删除
//delete data.n
Object.defineProperty(data, 'n', {
//这里的n和之前的data中的n同名,所以会被覆盖掉
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue
}
})
// 就加了上面几句,这几句话会监听 data,你不是想改源对象么,原对象的改动
//我也要管
const obj = {}
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj
}
由于我们把源对象myData的所有属性都复制到了value(例子中只有一个实际上是遍历data),然后把源对象删除,重新生成一个虚拟对象n,他只能通过get或者set访问,现在如果你还想改myData.n就无法绕过set的过滤
myData5.n = -1
console.log(`${data5.n},设置为 -1 失败了`)
//n=0
这里其实就可以窥见Vue内部对于data对象中的属性所进行的一系列操作,现在我们就可以对data的变化进行监听,并将变化后的值渲染进页面中,附上Vue源码中的proxy函数,代理之后对这个data进行监听(observe)
这个observer就是构造watcher实例的构造函数,这里的value参数就是data
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
如果data是一个数组那么会对数组遍历并对其进行getter/setter封装
这里就是getter/setter函数
做一个总结
1.对data对象进行改造,将源对象变成无法直接读取的getter/setter对象,并监听 2. 将这个对象设置一个代理对象,并暴露给外部进行访问,注意这里的data已经是改造后的data对象了
三、 
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性,这是因为Vue只检查第一层的对象是否定义,因此我们可以在已定义的对象上嵌套其他响应式属性
new Vue({
data: {
obj: {
a: 1 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`
})
由于这里b没有定义在data中,所以我们可以用Vue.set(object, propertyName, value)或者vm.$set
methods:{
Vue.set(this.obj, 'b', 2)
this.$set(this.obj,'b',2)
}
有时你可能需要为已有对象赋值多个新属性,比如使用 Object.assign() 或 _.extend()。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。
this.obj = Object.assign({}, this.obj, { a: 1, b: 2 })
如果我们的data中是一个数组怎么办
new Vue({
data: {
obj: {
array:["a","b","c"]
}
},
template: `
<div>
{{obj.b}}
<button @click="setD">set d</button>
</div>
`,
methods:{
setD(){
this.array.push("d")
}
}
})