July 11, 2023

Vue2源码(3):依赖收集 派发更新

1 What

程序在运行时,状态会不断发生变化。当状态发生变化后,需要重新渲染,得到最新的视图。

正是响应式赋予了状态变更 - 重新渲染的能力

通常来说,响应式的核心组成部分为:数据劫持、依赖收集、派发更新

之前一篇文章已经叙述过数据劫持的内容

数据劫持源码分析:https://icheng.github.io/2023/07/10/Vue2%E6%BA%90%E7%A0%81%EF%BC%9A%E6%95%B0%E6%8D%AE%E5%8A%AB%E6%8C%81%E5%8E%9F%E7%90%86/

下面来分析一下依赖收集、派发更新的源码原理

2 思考 3 个问题

我们知道了 Vue2 底层是通过 Object.defineProperty 来实现数据响应式的,但是单有这个还不够,因为数据可能没有在模板中使用

所以 Vue2,通过依赖收集来判断哪些用到的数据的变更需要触发视图更新,再通过派发更新通知视图进行更新渲染

那么我们先来考虑 3 个问题:

3 依赖收集中的各个类

先来看一张图

数据是被观察者,将数据标记为已观测数据后,数据就会有一个 Dep 实例

触发数据的 getter 时,就会进行依赖收集,将依赖集中管理

触发数据的 setter 时,就会进行派发更新,会通知 观察者Watcher 进行渲染更新

带着这张图,我们来分析一下过程中的 3 个类: Watcher、Dep、Observe

3.1 Dep

源码位置:vue-main\src\core\observer\dep.ts

每一个被观测的数据都会有一个 Dep 类实例,且 Dep 拥有一个唯一 id

subs 队列

Dep 内部维护一个 subs 队列

subs就是subscribers的意思,保存着依赖本数据的观察者 Watcher

Dep.target

全局变量,指向目前正在使用的 watcher

dep.depend()

Dep 类中有个一个 depend() 方法,进行依赖收集

但他不会直接将 watcher 存放在 subs 中

而是调用 Dep.target.addDep()

通知 watcher 先存放 dep (去重)

再调用 dep.addSub() 让 dep 存储该 watcher

这样便完成了双向存储,并且不会重复存储

dep.notify()

触发数据劫持的 setter 时会执行这个 dep.notify()

会遍历 subs

通知 watcher 执行 .update() 方法 去派发更新

3.2 Watcher

观察者,拥有一个唯一id

同时要注意的是,watcher有三种:render watcher、 computed watcher (lazy为true)、user watcher (user为true)

addDep()

这个方法上面说过了,用来双向存储

update()

派发更新

这里会进行判断:

如果 lazy 为 true,说明是计算属性,则将 dirty 设置为 true

或者就进行执行 queueWatcher 进行异步更新

异步更新文章:

https://icheng.github.io/2023/07/08/Vue2%E6%BA%90%E7%A0%81%EF%BC%9AnextTick%E5%8E%9F%E7%90%86/

get()

update() 后,进入异步更新,异步更新的 flushSchedulerQueue() 还是会执行这个get()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
get() {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm) // 调用函数
} catch (e: any) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
// "touch" every property so they are all tracked as
// dependencies for deep watching
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

3.3 Observe 依赖收集的入口

之前的文章讲过,Observer 会将数据标记已观测数据

并通过 Object.defineProperty 的 get/set 劫持数据的读取和修改

劫持数据 getter 时进行依赖收集,如果 Dep.target 存在,就调用 dep.depend()

劫持数据 setter派发更新,调用 dep.notify()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
if (__DEV__) {
dep.depend({
target: obj,
type: TrackOpTypes.GET,
key
})
} else {
dep.depend()
}
if (childOb) {
childOb.dep.depend()
if (isArray(value)) {
dependArray(value)
}
}
}
return isRef(value) && !shallow ? value.value : value
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val
if (!hasChanged(value, newVal)) {
return
}
if (__DEV__ && customSetter) {
customSetter()
}
if (setter) {
setter.call(obj, newVal)
} else if (getter) {
// #7981: for accessor properties without setter
return
} else if (!shallow && isRef(value) && !isRef(newVal)) {
value.value = newVal
return
} else {
val = newVal
}
childOb = !shallow && observe(newVal, false, mock)
if (__DEV__) {
dep.notify({
type: TriggerOpTypes.SET,
target: obj,
key,
newValue: newVal,
oldValue: value
})
} else {
dep.notify()
}
}
}

看到这里,是否细节太多绕晕了?

来个初始化时的整体流程,从宏观角度再过一遍

3 初始化流程

比如我们在模板中使用 {{name}}

组件挂载过程中,进行 new Watcher,创建一个渲染watcher

并传入参数:渲染更新方法 vm._update(vm._render())

源码位置:vue-main\src\core\instance\lifecycle.ts (mountComponent)

因为不是 computed watcher ,会默认执行 get() 方法:

看看源码:

源码位置:vue-main\src\core\observer\watcher.ts

1
this.value = this.lazy ? undefined : this.get()

4 修改值流程

那么修改值的过程,主要就是多了劫持getter和异步更新

About this Post

This post is written by Duan WeiCheng, licensed under CC BY-NC 4.0.