Vue2 - 数据响应式原理
1,总览

简单介绍下上图流程:以 Data 为中心来说,
- Vue 会将传递给 Vue 实例的 data 选项(普通 js 对象),通过
Object.defineProperty把这些property全部转为getter/setter。 - 当执行
render函数时,会触发用到的响应式数据的getter,getter会进行依赖收集并放到Watcher中。 - 当修改被收集到
Watcher中的响应式数据时,会触发setter,setter会通知Watcher来重新执行render函数更新DOM树,同时再次进行第2步,形成闭环重复整个流程。
template模板最终也会被编译为render函数执行。参考虚拟DOM树生成流程
响应式数据的目标:当对象本身或是属性发生变化时,会运行一些函数(最常见的是 render 函数)。
具体实现,vue 用到了几个核心部件。
- Observer
- Dep
- Watcher
- Schedule
2,Observer
目标:将传递给 Vue 实例的 data 选项(普通 js 对象)转化为响应式对象。
为了实现这点,Observer 把对象的每个属性通过 Object.defineProperty 转换为带有 getter/setter 的属性。这样当访问或修改这些属性时,vue 就可以做一些事情了。

Observer 是 Vue 内部的构造器,可以通过 Vue 提供的静态方法 Vue.observable(object) 间接使用该功能。
Vue.observable(object)的使用场景参考这篇文章。
时间点:发生在 beforeCreate 之后,created之前。
具体实现:递归遍历所有属性,以完成深度的属性转换。
而由于只能遍历已有的属性,所以无法监测到将来动态添加或删除的属性。因为提供了
$set和$delete这2个实例方法。
对于数组,为了监听那些可能改变数组内容的方法,vue 更改了数组的隐式原型。
vue 处理过后的数组 this.arr.__proto__上有7个方法可以被监听到。同时 this.arr.__proto__.__proto__ 指向真正的数组原型来正常使用数组的其他方法。

注意,直接修改数组的元素,是无法触发更新的。比如
this.arr[0] = 1。但是修改数组中某一个元素对象的属性时,是可以监听到的。比如this.arr[0].name = 'xxx'
总之,Observer 就是为了让一个对象属性的读取和赋值,内部数组变化等都可以被感知到。
3,Dep
作用和原理:
Vue会为响应式对象中的每个属性、对象本身、数组本身创建一个 Dep 实例(一个列表),每个 Dep 实例都可以做两件事:
- 记录(收集)依赖:当读取响应式对象的某个属性时,它会进行依赖收集。
- 派发更新:当改变某个属性时,它会派发更新。
function defineReactive(obj, key, val) {
let Dep; // 依赖
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: () => {
// 被读取了,将这个依赖收集起来
Dep.depend();
return val;
},
set: (newVal) => {
if (val === newVal) {
return;
}
val = newVal;
// 被改变了,派发更新
Dep.notify();
},
});
}

举例:
<!-- 组件 -->
<template>
<div>
<div>{{ obj.a }}</div>
<div>{{ arr }}</div>
<button @click="count++">修改conut</button>
</div>
</template>
<script>
// 会创建的 Dep 的元素:
export default {
data() {
return {
obj: { // Dep
a: 1, // Dep
b: 2
},
arr: [1, 23, 4], // Dep
count: 0
};
},
};
</script>
- 因为模板中使用了
obj.a,所以obj自身和a属性都会创建Dep。 count不会创建,是因为count其实并没有在模板中使用,而事件在渲染时不会运行。
有个问题,为什么要给对象自身也创建个 Dep,直接给用到的属性创建不可以吗?
不可以,因为直接修改对象(
this.obj = xxx),或通过this.$set(this.obj,"c", 3)或this.$set(this.obj,"a")增删属性时,都需要直接修改对象自身,才能完成响应式更新。
所以,最好一开始就定义好对象属性的初始值,来避免使用 this.$set 或 this.$delete 来触发对象自身的 Dep。
因为使用 this.obj.a 直接触发属性 a 的 Dep 效率会更好。
对一个属性来说,会收集依赖的有3个可能的位置(因为都需要响应式更新或执行):
- 模板,也就是
render()中。 this.$watch()中。computed()中。
4,Watcher
新的问题:Dep 是如何知道谁在用我的?换句话说,是谁触发的 getter 后执行的 Dep.depend()。
比如,某个函数执行时用到了响应式数据 a,a 怎么知道是哪个函数用的自己?
Vue的解决方式:Vue 不会直接执行函数,而是把函数交给一个叫 Watcher(一个对象)去执行。每个用到响应式数据的函数执行时,都会创建一个 Watcher,通过它来执行函数。
之后响应式数据变化时,Dep 会通知对应的 Watcher,去运行对应的函数来触发更新。

Watcher 大致原理:
首先有一个全局变量。
- 在执行给它的函数之前,将它的
this赋值给这个全局变量。 - 执行函数时,会触发响应式数据的
getter后执行的Dep.depend()。 - 在
Dep.depend()的逻辑中,会检查这个全局变量,从而确定是哪个Watcher。 - 函数执行完后清空全局变量。
所以,对于一个组件实例来说,都至少对应一个 Watcher ,它记录的是该组件的 render 函数。
Watcher 首先会运行一次 render 来收集依赖,于是 render 函数中用到的响应式数据都会记录这个 Watcher。
之后响应式数据变化,Dep 会通知这个 Watcher 来运行 render 函数触发更新,重新渲染页面同时再次收集当前的依赖。
打印组件的 this:

Watcher 触发更新时,会进行对比新旧虚拟DOM树,完成对真实DOM的更新。具体原理参考diff 的原理
5,Schedule
新的问题又出现了,假如 render 函数中使用的响应式数据有多个 a,b,c,d,那这些数据都会记录 Watcher,之后一次性修改这4个时,render 函数就会执行4次,效率岂不是很低。
实际上,Watcher 在收到派发更新的通知后,不是立即执行对应的函数,而是把自己交给一个叫Schedule(调度器)的东西。
调度器维护一个队列,相同的 Watcher 仅会存在一次(类似 Set)。这些 Watcher 也不是立即执行,而是把需要执行的 Watcher 放到事件循环的微队列中(通过工具方法 nextTick )。
所以当响应式数据发生变化时,执行的
render函数是异步的。
整体流程:

以上。
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。 如若内容造成侵权/违法违规/事实不符,请联系我的编程经验分享网邮箱:veading@qq.com进行投诉反馈,一经查实,立即删除!