一、从一个栗子开始
先看一个简单的例子,代码如下:
<template>
<div id="app">
<div class="student-info">
<p>name: {{ student.name}}</p>
<p>age: {{ student.age}}</p>
</div>
<button @click="addAgeHandler">Add Age</button>
<button @click="modifyAgeHandler">Modify Age</button>
</div>
</template>
<script>
export default {
name: "App",
data() {
return {
student: {
name: "Alex"
}
};
},
methods: {
// case 1
addAgeHandler() {
this.student.age = 18;
}
// case 2
addAgeHandler() {
this.$set(this.student, "age", 18);
}
modifyAgeHandler() {
this.student.age = 20;
}
}
};
</script>
在上述代码中,我写了两个addAgeHandler函数,两个函数中都会将student修改为{name:'Alex',age:18}。但是这两个函数的效果是一样的吗,是否会将age的数值正确的显示在页面上呢?
实际的情况是:
-
case 1: 点击Add Age后不显示age的数值,点击Modify Age后依旧不显示age的数值
-
case 2: Add Age和Modify Age都会正常显示age的数值
不知道这是不是有些出乎你的意料,你可能会疑惑为什么case 1中student.age修改后没有渲染到页面上,接下来将为你解答。
二、case 1的问题分析
要搞清楚为什么case 1无法触发age渲染到页面上,我们先要弄明白数据与渲染函数之间的响应式关系是如何建立的。
-
vue组件初始化时会对data中的所有数据进行observe,observe时会用Object.defineProperty对数据的get和set方法进行拦截,在get中进行依赖收集,在set方法中触发依赖更新;
-
在vue组件渲染时,template中使用的变量会被读取,从而触发上一步中设置的get方法,这样就将渲染函数添加到了变量的依赖列表中;
-
当变量被修改时,执行set方法,触发其所有的依赖进行更新,渲染函数被重新执行,组件被重新渲染。
在我们的的代码中,初始化时student中没有age属性,因此age并没有被observe,从而在页面渲染时即使template中有{{student.age}}也无法将渲染函数收集到age的依赖列表中(因为studet.age没有被observe,没有拦截后的get方法执行,当然也无法收集依赖了)。
这就是为什么case 1中虽然修改了student.age,但是无法触发vue组件重新渲染,如果想使用case 1达到重新渲染的目的,可以这样处理:
data() {
return {
student: {
name: "Alex",
age:undefined
}
};
}
初始化时就明确定义student.age属性,那么在初始化时就会对age属性进行observe,在vue组件渲染时触发了依赖收集,从而修改student.age会触发组件重新渲染。
三、case 2的原理分析
接下来我们来看case 2为什么可以触发组件重新渲染,在case 2中使用Vue提供的全局Api-this.$set完成student.age赋值,看一下set方法简化版的源码
// vue源码\src\core\observer\index.js
export function set (target, key, val) {
// 省略target是array情况下的处理
......
// target的observer
const ob = (target).__ob__
// 将target.key设置成响应式
defineReactive(ob.value, key, val)
// 通知target的依赖更新
ob.dep.notify()
return val
}
我们理一下代码的执行顺序:
-
this.$set(this.student,'age',18)执行;
-
defineReactive将student.age设置成响应式,对其添加了get和set拦截;
-
ob.dep.notify()通知student的依赖进行更新,student的依赖中包含了组件的渲染函数,从而触发组件重新渲染 ( 或许你会渲染函数何时被收集到student的依赖中了,这是因为渲染时会访问student.name和student.age,肯定是先访问student,才能访问到student的属性,所以student的get方法会被调用,渲染函数也就被收集到student的依赖中了 ) ;
-
组件渲染过程中,遇到变量student.age,触发student.age的get拦截,从而在student.age的依赖中加入组件的渲染函数,完成依赖收集;所以在modify age的时候,直接修改student.age就可以触发组件的重新渲染啦。
值得注意的是,在case 2的addAgeHandler中,引发组件重新渲染的是ob.dep.notify()这句代码,即student的依赖更新导致,而不是student.age的依赖更新导致。modifyAgeHandler中引发组件渲染的才是student.age收集的依赖。虽然都是渲染函数的重新执行,但是触发的主体不一样。