1 What
程序在运行时,状态会不断发生变化。当状态发生变化后,需要重新渲染,得到最新的视图。
正是响应式赋予了状态变更 - 重新渲染的能力
通常来说,响应式的核心组成部分为:数据劫持、依赖收集、派发更新
之前一篇文章已经叙述过数据劫持的内容
下面来分析一下依赖收集、派发更新的源码原理
2 思考 3 个问题
我们知道了 Vue2 底层是通过 Object.defineProperty
来实现数据响应式的,但是单有这个还不够,因为数据可能没有在模板中使用
所以 Vue2,通过依赖收集来判断哪些用到的数据的变更需要触发视图更新,再通过派发更新通知视图进行更新渲染
那么我们先来考虑 3 个问题:
- 依赖是谁?换句话说,属性发生变化后,通知到谁?(Watcher)
- 依赖收集到哪里?每一个数据的依赖都需要集中管理(Dep)
- 在哪里做依赖收集?(Observe)
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
- targetStack:栈结构,用来保存Watcher
- pushTarget:往 targetStack 中 push 当前的 Watcher(排在前一个Watcher的后面),并把 Dep.target 赋值给当前 Watcher
- popTarget:先把 targetStack 最后一个元素弹出(.pop),再把 Dep.target 赋值给最后一个Watcher(也就是还原了前一个Watcher)
- 通过上述实现,vue 保证了 全局唯一的 Watcher,准确赋值在 Dep.target 中
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()方法
- 调用dep的pushTarget
- 调用回调函数(如渲染更新方法
vm._update(vm._render())
) - popTarget
1 | get() { |
3.3 Observe 依赖收集的入口
之前的文章讲过,Observer 会将数据标记已观测数据
并通过 Object.defineProperty 的 get/set 劫持数据的读取和修改
劫持数据 getter
时进行依赖收集,如果 Dep.target
存在,就调用 dep.depend()
劫持数据 setter
时派发更新,调用 dep.notify()
1 | Object.defineProperty(obj, key, { |
看到这里,是否细节太多绕晕了?
来个初始化时的整体流程,从宏观角度再过一遍
3 初始化流程
比如我们在模板中使用 {{name}}
组件挂载过程中,进行 new Watcher,创建一个渲染watcher
并传入参数:渲染更新方法 vm._update(vm._render())
源码位置:vue-main\src\core\instance\lifecycle.ts (mountComponent)
因为不是 computed watcher ,会默认执行 get() 方法:
pushTarget() :当前 Dep.target 指向这个渲染watcher
调用回调函数,当前函数是渲染更新方法
vm._update(vm._render())
首先
vm._render()
会去实例中取新值,这个过程则调用Object.defineProperty
的 get,当前Dep.target
是存在的,会执行dep.depend()
,进行依赖收集那么 dep 和 watcher 的双向存储就完成了
然后
vm._update()
会将虚拟DOM转为真实DOM,涉及diff算法等等,总之渲染完成popTarget(): Dep.target 不需要指向这个渲染watcher了
同时会存储目前的属性值,存储在value,方便后面判断值是否修改了
看看源码:
源码位置: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.