写在前面
我相信很多同学对Vue的数据响应式是通过Vue.js文档来了解的,但毕竟文档的篇幅有限,你能知道自己理解了多少吗?先来看看下面几道题目,来看看你的理解是否到位。
题目一
//HTML
<div id="app">
<span class=span-a>
{{obj.a}}
</span>
<span class=span-b>
{{obj.b}}
</span>
</div>
//JS
var app = new Vue({
el: '#app',
data: {
obj: {
a: 'a',
}
},
})
app.obj.a = 'a2'
问:请问最终 span-a 和 span-b 中分别展示什么字符串?
题目二
//HTML
<div id="app">
<span class=span-a>
{{obj.a}}
</span>
<span class=span-b>
{{obj.b}}
</span>
</div>
//JS
var app = new Vue({
el: '#app',
data: {
obj: {
a: 'a',
}
},
})
app.obj.b = 'b'
问:请问最终 span-a 和 span-b 中分别展示什么字符串?
题目三
//HTML
<div id="app">
<span class=span-a>
{{obj.a}}
</span>
<span class=span-b>
{{obj.b}}
</span>
</div>
//JS
var app = new Vue({
el: '#app',
data: {
obj: {
a: 'a',
}
},
})
app.obj.a = 'a2'
app.obj.b = 'b'
问:请问最终 span-a 和 span-b 中分别展示什么字符串?
好了,以上三题你都能想明白吗?带着这些疑问我们来深入了解一下Vue的数据响应式。
深入理解
什么是数据响应式
先聊一聊,什么是响应。
“响应”,中文的意思也就是“回应”。比如,别人叫你一声或者给你发消息,你回复了他,这个过程就叫响应。
那什么是数据响应式呢?
Vue的官方文档已经很明确的告诉我们了。对于数据data,只要你修改了,视图就会自动更新,不需要你自己再去操作DOM。这也是Vue 最独特的特性之一 —— 非侵入性的响应式系统。
怎么理解“非侵入性”呢,我觉得的可以理解为“不可篡改的”,即Vue做到了只要你修改了data中的任意数据,对应的组件实例的watcher实例都会收到消息,并重新渲染与其关联的组件。
通过一个例子理解内部原理
接下来我们一起来了解一下,这一机理的背后是怎样进行的,我们来看下面这个例子:
进入实例前,大家需要了解以下前置知识:
假设有这样一个需求:我们需要存储一个n的值,有一个条件就是n不能小于0。
请问我们怎样保证,不管用户怎么修改n的值,都可以满足我们的要求。
let myData = {n:0}
let data = proxy({ data:myData }) // 括号里是匿名对象,无法访问
function proxy({data}){
//监听data的变化
let value = data.n // 声明一个新的 value 来获取 data.n 这样就可以监听 data.n 的变化
Object.defineProperty(data, 'n', {
get(){
return value
},
set(newValue){
if(newValue<0)return
value = newValue
}
})
//添加代理
const obj = {}// 声明一个新的对象,把 data.n 全权负责给 obj,这样无轮怎样篡改,
// 都不会影响 n 的变化
Object.defineProperty(obj, 'n', {
get(){
return data.n
},
set(value){
if(value<0)return
data.n = value
}
})
return obj // obj 就是代理
}
【代码分析】
- 首先,给我们要存储n的值的对象命名为myData
- 对这个对象的操作,我们交给一个代理来完成,这个代理就是data
- 为了防止用户修改myData,我们先对代理data进行监听
- 这里我们声明了一个新的value来存储data.n的原始值
- 接着定义一个新的(虚拟)n来覆盖原来的data.n,如果你要读取n的值,就调用get函数;如果你要设置新的值,就把新的值给value
- 下一步,添加代理
- 声明一个新的对象,把 data.n 全权负责给 obj,这样无轮怎样篡改,都不会影响 n 的变化
- 这里返回了obj,所以data就是代理
好了,说了这么多,请问和vue的数据响应式有什么联系呢?
别急,我们再来看一个例子。
//引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
const myData = {
n: 0
}
console.log(myData) // 重点一
new Vue({
data: myData,
template: `
<div>{{n}}</div>
`
}).$mount("#app");
setTimeout(()=>{
myData.n += 10
console.log(myData) //重点二
},3000)
打印结果为:
是不是很奇怪。
为什么把 data 在外部创建,在 Vue 里引用,然后在创建后和引用后分别打印一次,两次打印出来的n会不一样。
这是因为,Vue 对 data 里的数据进行了一些处理。
那又是什么样的处理?
我在第一个例子就已经告诉你了。
有没有觉得 let data = proxy({ data:myData })
看着很眼熟。
没错,Vue在创建实例的时候, const vm = new Vue({data:myData})
,也是如此。
用文档里的话来解释就是:
- 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的 property,并使用 Object.defineProperty 把这些 property 全部转为 getter/setter。
- 这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 property 被访问和修改时通知变更。
- 每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据 property 记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
说人话就是:
- Vue 让 vm 成为了 myData 的代理,并且会对 myData 的所有属性进行监控。
- 什么是代理? 就是当你对myData对象的属性进行读写的时候,全权由另一个对象vm来负责,那么vm就是myData的代理。比如,不用myData.n,偏要用vm.n来操作myData.n。
- 为什么要监控? 就是为了防止myData的属性变了,vm不知道
- vm知道了又如何? 知道属性变了就可以调用render(data)啊~~~
- 这样,就实现了只要修改了data.n,Vue就会自动帮你实现视图的重新渲染,即UI里的n就会响应我,这就是响应式的过程。
扩展
Vue似乎有个bug
好了,经过上面的分析,我相信同学们都能理解Vue数据响应式的原理了。
但是还没结束,这里有几个有意思的例子,带大家看一下。
- 例一
我们知道,Vue是通过 Object.defineProperty(obj, 'n', {...})
来实现监听和代理obj.n的,如果我们忘记给 'n'
了会怎么样?
//引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
new Vue({
data: {},
template: `
<div>{{n}}</div>
`
}).$mount("#app");
结果为:
浏览器会抛出一个警告
- 例二
//引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
this.obj.b = 1; //请问,页面中会显示 1 吗?
}
}
}).$mount("#app");
请问:此时我点击set n,视图会显示1吗
答案是:不会
为啥:因为Vue没法监听一开始不存在的obj.b
解决方案
方案一:提前声明好所有的key,后面再加属性
举例:
//引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
new Vue({
data: {
n:undefined
},
template: `
<div>{{n}}</div>
`
}).$mount("#app");
方案二:使用Vue.set或this.$set
作用:
- 新增key
- 如果没有创建过,自动创建代理和监听
- 触发视图更新(异步的,不会立刻更新)
举例:
//引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
new Vue({
data: {
obj: {
a: 0 // obj.a 会被 Vue 监听 & 代理
}
},
template: `
<div>
{{obj.b}}
<button @click="setB">set b</button>
</div>
`,
methods: {
setB() {
Vue.set(this.obj,'b',1)
// this.$set(this.obj,'b',1) //这句和上面等同
}
}
}).$mount("#app");
针对数组的解决方案
我们很容易想到,如果是数组的话,没有办法提前就把后面要设置的属性就写好,那要怎么解决呢。
方案一:可以用Vue.set或this.$set(不推荐)
方案二:尤雨溪为我们提供了一种数组的变异方法
举例:
//引用完整版 Vue
import Vue from "vue/dist/vue.js";
Vue.config.productionTip = false;
new Vue({
data: {
array: ["a", "b", "c"]
},
template: `
<div>
{{array}}
<button @click="setD">set d</button>
</div>
`,
methods: {
setD() {
// this.$set(this.array,3,'d') //可以实现,不推荐
this.array.push('d') //可以实现,推荐
}
}
}).$mount("#app");
我们查看一下控制台:
我们发现尤雨溪对着7个API进行了改造,方便你虽数组进行增删,这7个API会自动处理监听和代理,并更新视图。具体内容可以参考文档中的变异方法章节。
回答一下最开始的问题
好了,现在我们可以回答最开始的题目了。
【题目一】
分析:
- 由于obj.a提前定义了,后面又被改写为a2,所以会被监听;
- 由于obj.b没有事先定义,所以不会被监听。
结论: span-a 中显示a2,span-b 中不显示。
【题目二】
分析:
- 由于obj.a提前定义了,所以会被监听;
- 由于obj.b没有事先定义,所以不会被监听。
结论: span-a 中显示a,span-b 中不显示。
【题目三】
分析:
- 由于obj.a提前定义了,后面又被改写为a2,所以会被监听。这个没什么问题,但是后面的就有问题了
- 大家会不会以为,obj.b没有事先定义,所以不会被监听。
- 如果是这样想的话,那就错了,实际上span-b 中会显示b
深入分析:
- 要理解为什么 span-b 会更新,要点是理解视图更新其实是异步的。
- 当我们让 a 从 'a1' 变成 'a2' 时,Vue 会监听到这个变化,但是 Vue 并不能马上更新视图,因为 Vue 是使用 Object.defineProperty 这样的方式来监听变化的,监听到变化后会创建一个视图更新任务到任务队列里。
- 所以在视图更新之前,要先把余下的代码运行完才行,也就是会运行 b = 'b'。
- 等到视图更新的时候,由于 Vue 会去做 diff(文档中有写),于是 Vue 就会发现 a 和 b 都变了,自然会去更新 span-a 和 span-b。
结论: span-a 中显示a2,span-b 中显示b。
小结
通过以上分析,相信大家对Vue的数据响应式原理已经有了一定的理解。我觉得,关键是要多看文档,多思考、多总结,通过写一些demo来验证自己的猜想,这样你可以不用看源码就能知道这个技术的一些细节。
知乎:Paula Hu