Vue3源码精读03:响应式核心依赖追踪机制|track与trigger底层源码全解析
前置阅读: Vue3源码精读02:核心基础响应式API全解析
一、前言
在前一篇文章中,我们彻底理清了 ref、reactive、shallow、toRefs 等基础响应式 API 的底层创建逻辑。我们知道 Proxy 可以拦截数据的读取、修改、删除操作。
但 单纯的劫持并不等于响应式。
Proxy 仅仅能"感知"数据变化,它并不知道:
- 哪些组件、函数使用了当前数据?
- 数据变了,需要更新哪些视图、执行哪些逻辑?
真正支撑 Vue3 自动更新闭环的,是依赖追踪系统。
整个响应式系统的核心闭环由两步构成:
- track(依赖收集) :数据被读取时,记录当前正在运行的副作用函数
- trigger(触发更新) :数据被修改时,执行之前收集到的所有副作用函数
组件渲染、watch 监听、computed 计算属性,底层全部依赖这套机制运行。本文将从数据结构设计、执行时序、源码逐行拆解、边界容错四个维度,深度解析 Vue3 依赖追踪的底层原理。
二、依赖存储核心架构:三层嵌套数据结构
Vue3 为了精准管理全局所有响应式依赖,设计了一套 WeakMap + Map + Set 的三层嵌套结构,统一存储在全局变量 targetMap 中。
typescript
const targetMap = new WeakMap<
object,
Map<string | symbol, Set<ReactiveEffect>>
>()
2.1 三层结构层级含义
- 第一层 WeakMap:target → depsMap key 是原始目标对象,WeakMap 是弱引用类型,当原始对象销毁时,对应的依赖数据会自动被 GC 回收,从底层规避内存泄漏。
- 第二层 Map:key → dep key 是对象的属性名,实现属性级依赖隔离,修改对象 A 属性,只会触发 A 属性对应的更新,不会冗余更新其他属性。
- 第三层 Set:Set 存储当前属性对应的所有副作用函数,Set 自带去重特性,避免同一个副作用被重复收集,造成无效执行。
2.2 结构可视化
markdown
targetMap(全局根容器)
└── 原始响应式对象 target
└── depsMap(属性依赖映射表)
├── 属性name → 存储对应 effect 集合
└── 属性age → 存储对应 effect 集合
三、副作用核心类:ReactiveEffect 与 activeEffect
依赖收集的本质,是收集「依赖当前数据的函数」,这类函数在 Vue3 源码中被称为 副作用 ReactiveEffect。
同时 Vue3 通过全局变量 activeEffect,标记当前正在执行的副作用,这是依赖收集的核心开关。
typescript
// 全局标记:当前正在运行的副作用
let activeEffect: ReactiveEffect | undefined
class ReactiveEffect {
// 原始副作用函数
private _fn: () => any
// 是否激活
active = true
// 反向存储:当前effect被哪些依赖集合收集
deps: Set<ReactiveEffect>[] = []
constructor(fn: () => any) {
this._fn = fn
}
// 执行副作用函数
run() {
// 非激活状态直接执行,不收集依赖
if (!this.active) {
return this._fn()
}
// 关键:标记当前运行的副作用
activeEffect = this
// 执行函数,触发数据读取,进行依赖收集
const result = this._fn()
// 执行完毕清空,防止误收集无关依赖
activeEffect = undefined
return result
}
}
3.1 核心执行逻辑
当副作用执行 run 方法时:
- 将当前 effect 实例赋值给
activeEffect; - 执行用户传入的回调函数,函数内部读取响应式数据;
- 触发 Proxy get 拦截,执行 track 收集当前 activeEffect;
- 函数执行完毕,清空 activeEffect,关闭依赖收集。
核心结论:只有 activeEffect 存在时,读取数据才会收集依赖。
四、track 源码逐行解析:依赖收集全过程
track 函数在 Proxy 的 get 捕获器、RefImpl 的 value 读取器中触发,作用是追踪数据依赖,存储副作用。
csharp
export function track(
target: object,
type: TrackOpTypes,
key: string | symbol
) {
// 没有正在运行的副作用,直接终止,无需收集
if (!activeEffect) {
return
}
// 1. 查找当前对象的依赖映射表
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 2. 查找当前属性对应的副作用集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 3. 去重收集副作用
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
// 反向记录:让effect保存所属的依赖集合,用于后续清理
activeEffect.deps.push(dep)
}
}
4.1 track 核心规则与底层原理
- 无 activeEffect 不收集依赖:普通同步代码、setTimeout、Promise 异步回调中读取数据,不会收集依赖,修改数据无法更新视图。
- Set 自动去重:同一副作用多次读取同一属性,只会收集一次,避免重复执行。
- 双向绑定机制 :数据存储 effect,effect 反向存储 dep,为
effect.stop()清理依赖提供支撑。
五、trigger 源码逐行解析:派发更新全过程
当响应式数据被修改、删除、新增时,会触发 set / delete 捕获器,调用 trigger,取出对应依赖的副作用函数批量执行,完成视图更新。
typescript
export function trigger(
target: object,
type: TriggerOpTypes,
key?: string | symbol,
newValue?: unknown
) {
// 无依赖映射,直接返回
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
// 获取当前属性对应的所有副作用
let deps: Set<ReactiveEffect> | undefined
if (key !== undefined) {
deps = depsMap.get(key)
}
// 拷贝新集合,避免原集合遍历过程中被修改引发异常
const effectsToRun = new Set<ReactiveEffect>()
if (deps) {
deps.forEach((effect) => {
// 关键容错:禁止当前运行的effect触发自身,防止死循环
if (effect !== activeEffect) {
effectsToRun.add(effect)
}
})
}
// 批量执行所有副作用
effectsToRun.forEach((effect) => {
effect.run()
})
}
5.1 两大核心容错设计解读
1. 拷贝集合 effectsToRun
在执行 effect.run 的过程中,会重新触发 track,可能新增、删除原 dep 集合中的数据。JS 遍历原生 Set 时,集合变动会导致遍历错乱。拷贝副本遍历,保证更新逻辑稳定。
2. 禁止自循环 effect !== activeEffect
经典死循环场景:
scss
const state = reactive({ count: 0 })
effect(() => {
state.count++
})
如果没有该判断:effect 执行 → 修改数据 → trigger 再次执行 effect,无限递归。源码通过跳过当前活跃 effect,彻底杜绝自循环死锁问题。
六、完整联动流程示例
下面通过最简示例,串联「effect 执行 → track 收集 → trigger 更新」完整闭环。
scss
const state = reactive({ num: 1 })
// 创建副作用
effect(() => {
console.log(state.num)
})
// 修改数据,触发更新
state.num = 100
6.1 完整时序拆解
effect(fn)创建 ReactiveEffect 实例,调用 run 方法;- run 赋值
activeEffect = 当前实例; - 执行回调函数,读取
state.num,触发 Proxy get; - 执行 track,将当前 effect 存入 targetMap 对应属性集合;
- 回调执行完毕,清空 activeEffect;
- 赋值
state.num = 100,触发 Proxy set; - 执行 trigger,取出对应属性的所有副作用;
- 遍历执行 effect.run,重新渲染、执行逻辑。
七、ref 如何复用同一套追踪机制
reactive 依靠 Proxy 劫持自动触发 track/trigger,而 ref 没有 Proxy,直接手动调用 track/trigger,完全复用同一套依赖系统。
kotlin
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(value: T) {
this._value = value
}
// 读取.value 手动收集依赖
get value() {
track(this, TrackOpTypes.GET, 'value')
return this._value
}
// 修改.value 手动触发更新
set value(newVal) {
if (hasChanged(newVal, this._value)) {
this._value = newVal
trigger(this, TriggerOpTypes.SET, 'value')
}
}
}
ref 的依赖存储逻辑完全一致:target 是 RefImpl 实例,固定 key 为 value。
八、底层机制衍生的常见业务问题
8.1 reactive 解构丢失响应式
解构会直接获取原始值,脱离 Proxy 代理环境,后续读取不会触发 get、不会执行 track,无法收集依赖,响应式彻底失效。这也是toRef/toRefs 必须存在的底层原因。
8.2 异步回调修改数据不更新视图
setTimeout、Promise.then 执行时,副作用早已执行完毕,activeEffect 已清空,读取数据无法收集依赖。
8.3 多次赋值不会死循环
得益于 trigger 内部 effect !== activeEffect 的自循环拦截机制。
8.4 对象销毁自动释放依赖
顶层采用 WeakMap 存储,原始对象销毁后,对应依赖自动回收,降低内存占用。
九、全文总结
本文完整拆解了 Vue3 响应式系统的核心内核------依赖追踪机制,核心要点汇总:
- 采用 WeakMap + Map + Set 三层结构,精准隔离属性依赖,自动回收内存;
- ReactiveEffect 统一管理所有副作用,activeEffect 作为依赖收集开关;
- track:读取数据收集依赖,无活跃副作用则不收集;
- trigger:修改数据派发更新,通过副本遍历、自循环拦截规避边界异常;
- ref 与 reactive 底层复用同一套追踪体系,实现全场景响应式统一。