简单介绍下上图流程:以 Data 为中心来说,
Object.defineProperty
把这些 property
全部转为 getter/setter
。render
函数时,会触发用到的响应式数据的 getter
,getter
会进行依赖收集并放到 Watcher
中。Watcher
中的响应式数据时,会触发 setter
,setter
会通知 Watcher
来重新执行 render
函数更新DOM树,同时再次进行第2步,形成闭环重复整个流程。
template
模板最终也会被编译为render
函数执行。参考虚拟DOM树生成流程
响应式数据的目标:当对象本身或是属性发生变化时,会运行一些函数(最常见的是 render
函数)。
具体实现,vue 用到了几个核心部件。
目标:将传递给 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 就是为了让一个对象属性的读取和赋值,内部数组变化等都可以被感知到。
作用和原理:
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
不会创建,是因为事件在渲染时不会运行。有个问题,为什么要给对象自身也创建个 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()
中。新的问题: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
:
新的问题又出现了,假如 render
函数中使用的响应式数据有多个 a
,b
,c
,d
,那这些数据都会记录 Watcher
,之后一次性修改这4个时,render
函数就会执行4次,效率岂不是很低。
实际上,Watcher
在收到派发更新的通知后,不是立即执行对应的函数,而是把自己交给一个叫Schedule(调度器)的东西。
调度器维护一个队列,相同的 Watcher
仅会存在一次(类似 Set
)。这些 Watcher
也不是立即执行,而是把需要执行的 Watcher
放到事件循环的微队列中(通过工具方法 nextTick
)。
所以当响应式数据发生变化时,执行的
render
函数是异步的。
整体流程:
以上。