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进行投诉反馈,一经查实,立即删除!