目录
- 1,总览
- 2,Observer
- 3,Dep
- 4,Watcher
- 5,Schedule
1,总览
vue2官网参考

简单介绍下上图流程:以 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: { // Depa: 1, // Depb: 2},arr: [1, 23, 4], // Depcount: 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函数是异步的。
整体流程:

以上。