Vue监测数据改变的原理
**数据:**指的是存在data中的数据。
如果我们修改了data中存储的数据,Vue会监测到这种修改,并且把修改反应到页面上。
Vue的这种监测是通过Vue上默认的监视实现的。与watch不同,watch是Vue提供给开发者使用的方法。但不管是默认监视,还是watch,背后的原理都类似。
了解Vue的监视原理是很重要的,如果不知道,当自己写的某种数据修改不能被Vue识别到,就很难去分析成因。
一种Vue无法监视到数据修改的情况:
如果用访问下标的方法替换数组中某个对象元素,把整个元素都替换成新的,虽然后台数据已经修改了,但Vue无法检测到这种修改,因此页面的数据也无法改变。
而且,有时候,页面上的数据没有改变,但通过控制台输出修改后的数据,再打开开发者工具,开发者工具中的数据会发生改变,但页面上的内容仍然不变,这种错乱的现象令人迷惑。
html
<body>
<div id="root">
<button @click="changeData">change</button>
<ul>
<li v-for="(data,index) in listData" :key="data.id">
{{ data.name }}:{{data.age}} -- {{index}}
</li>
</ul>
</div>
<style>
.styleBackgroundColor{
background-color: aqua;
}
.styleContent{
width:300px;
height: 200px;
}
.styleBorder{
border: 2px black solid;
}
</style>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
let vm = new Vue({
el: '#root',
data() {
return {
listData:[
{name:'catcat',age:5,id:'001'},
{name:'dogcat',age:8,id:'002'},
{name:'QAQ',age:15,id:'003'},
{name:'QAQTAT',age:15,id:'004'},
{name:'123',age:15,id:'005'},
],
}
},
methods:{
changeData(){
this.listData[0] = {name:'test',age:'changed',id:'001'};
}
}
})
</script>
</body>
在这个例子中,页面上显示原始数组信息,当点击change按钮时,修改数组中的第一条数据,但这时令人迷惑的情况发生了,页面上的数据并不会发生改变,但在控制台打印listData数组,后台的数据已经被更新,这种更新并没能传递到页面上:

这与Vue的监测机制相关。
Vue如何监测对象数据的改变:
Vue会对data中的数据加工,生成_data,_data中为每个属性都添加了set和get,当data中数据改变时,会触发数据对应的set函数,set函数的执行代表有数据发生变化,会引起Vue模版的重新解析,Vue模版会使用新的数值解析指令,最终在页面上显示出新的数据。
要对数据进行监测,要靠Observer函数进行实现,Observer函数是一个构造函数,能够创建一个监视的实例对象。
在Observer构造函数内部,首先会汇总对象中所有属性,并形成数组。然后会遍历这个数组,为Observer实例对象添加所有属性,对每个属性设置get和set,在get函数内部,返回被监视data的属性值,在set函数内部,把被监视data的属性值修改掉。
简单的模拟代码如下(实际vue底层代码要更复杂和完善一些):
javascript
<script>
let data = {
name: 'catcat',
age: 10,
}
function Observer(obj){
//获取数据中的所有属性
let keys = Object.keys(obj);
for(const k of keys){
//把所有属性添加到Observer实例上,并且增加set和get方法
Object.defineProperty( this, k , {
get(){
return obj[k];
},
set(value){
console.log('发现修改,重新进行模版解析等行为');
obj[k] = value;
}
} )
}
}
//创建监听实例
let obs = new Observer(data);
const vm = {};
vm._data = data = obs;
</script>
对于这段代码,当前对data数据只考虑了一层数值,当data数值是对象时,当前的代码对对象并不生效。但Vue会对对象及对象内嵌套的对象进行递归,为每层的数据都添加get和set,直到没有层级结构。Vue对藏在数组中的对象,也都为对象的属性赋予了get和set。
对于下列这种数据:
javascript
<script>
const vm = new Vue({
data:{
a:{
f:100,
b:{
g:'123123',
c:{
d:1,
e:2,
}
}
},
list:[
{
name:'a',age:1,
},
{
name:'n',age:2,
}
],
},
})
</script>

可以看到a以及内层的bf,b内层的cg,c内层的de,上面都有get和set。
对于list,list内部的对象属性也有get和set。

Vue.set
对于data中的数据,在vue实例创建阶段配置好的数据,是响应式的,但对于后加入的数据,由于没有set和get,并不是响应式的。
比如:
html
<body>
<div id="id">
<h2>{{a.cat}}</h2>
</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const vm = new Vue({
el:'#id',
data:{
a:{
f:100,
b:{
g:'123123',
c:{
d:1,
e:2,
}
}
}
},
})
</script>
</body>
对于这段代码,页面上显示了data中的a.cat,如果后续给a添加了cat属性,页面上还是无法显示出cat,因为后添加的cat并不是响应式的,页面无法监测到后台新加了数据。

如果希望后添加的数据也有响应式,需要使用Vue上提供的一个api。
Vue.set(target 往谁的身上追加属性,key 要追加的属性名 字符串格式,value 追加的属性值)

除了Vue.set,Vue实例对象上也有一个相同功能的方法,vm.$set。语法是一模一样的。

要找到追加属性的位置,可以不用vm._data.a,用vm.a也可以。
html
<body>
<div id="id">
<button @click="add">addData</button>
<h2>{{a.cat}}</h2>
</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const vm = new Vue({
el:'#id',
data:{
a:{
f:100,
}
},
methods:{
add(){
Vue.set(this.a,'cat','ttt');
//this.$set(this.a,'cat','ttt');
}
}
})
</script>
</body>
不过,set Api也有一些缺点。set Api只能给data中某一个对象追加属性,不能够给data追加属性。vm和vm._data不能作为target。
Vue如何监测数组数据的改变:
对于数组数据,vue不会为数组内部的元素添加set和get。Vue对数组其实也有监视机制,只不过不是通过set和get进行,因此通过下标修改数组元素,Vue不会监听到这种修改。
html
<body>
<div id="id">
<button @click="change">change</button>
<h2 v-for="data in list">{{data}}</h2>
</div>
<script type="text/javascript" src="https://cdn.jsdelivr.net/npm/vue@2/dist/vue.js"></script>
<script>
const vm = new Vue({
el:'#id',
data:{
list:[
'123',
{name:'a',age:2},
100
]
},
methods:{
change(){
console.log('change');
this.list[0] = 'aa';
}
}
})
</script>
</body>
页面上有change按钮,当点击按钮的时候修改数组第一条数据,但点击按钮之后,页面上的数据并不会发生变化。

当对数组数据使用push pop shift unshift splice sort reverse这些能够修改数组本身的方法时,Vue才能监测到。
javascript
methods:{
change(){
console.log('change');
this.list.shift();
this.list.unshift('aa');
}
}

对于不修改数组本身的方法,比如filter,只使用filter也是无法被vue监测到的,需要把filter的返回值赋值给整个数组,vue才能监测到这种修改。
因此,回到最开始的问题,为什么把数组第一条数据用下标修改之后,页面上的数据无法进行更新,是因为vue无法监测到对下标访问的修改。
**vue是如何监测到对数组使用了修改数组本身的方法:**vue使用了包装的手段,也就是说,在vue中调用数组的push等方法,这时使用的并不是Array原型对象上的push,而是vue自己写的push。在vue的push中,一开始还是调用了array原型上的push,然后,vue会重新解析模版,生成新的虚拟DOM,diff算法,然后更新页面。

除了使用push pop等方法修改数组,能实现响应式,用Vue.set vm.$set Api也可以响应式修改数组。
**数据劫持:**对于一个数据,把数据及数据内层所有子变量都赋予一个get和set,把这种动作行为叫做劫持。当有人修改或读取数值时,都会被劫持,进入set或get。