Vue3源码精读03:响应式核心依赖追踪机制|track与trigger底层源码全解析

Vue3源码精读03:响应式核心依赖追踪机制|track与trigger底层源码全解析

前置阅读: Vue3源码精读02:核心基础响应式API全解析


一、前言

在前一篇文章中,我们彻底理清了 refreactiveshallowtoRefs 等基础响应式 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 三层结构层级含义

  1. 第一层 WeakMap:target → depsMap key 是原始目标对象,WeakMap 是弱引用类型,当原始对象销毁时,对应的依赖数据会自动被 GC 回收,从底层规避内存泄漏。
  2. 第二层 Map:key → dep key 是对象的属性名,实现属性级依赖隔离,修改对象 A 属性,只会触发 A 属性对应的更新,不会冗余更新其他属性。
  3. 第三层 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 方法时:

  1. 将当前 effect 实例赋值给 activeEffect
  2. 执行用户传入的回调函数,函数内部读取响应式数据;
  3. 触发 Proxy get 拦截,执行 track 收集当前 activeEffect;
  4. 函数执行完毕,清空 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 完整时序拆解

  1. effect(fn) 创建 ReactiveEffect 实例,调用 run 方法;
  2. run 赋值 activeEffect = 当前实例
  3. 执行回调函数,读取 state.num,触发 Proxy get;
  4. 执行 track,将当前 effect 存入 targetMap 对应属性集合;
  5. 回调执行完毕,清空 activeEffect;
  6. 赋值 state.num = 100,触发 Proxy set;
  7. 执行 trigger,取出对应属性的所有副作用;
  8. 遍历执行 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 底层复用同一套追踪体系,实现全场景响应式统一。

相关推荐
睡觉时不困4421 小时前
Obsidian 三端同步完整流程:电脑、手机、平板通过 Gitee 实时同步
前端
渣波1 小时前
React Hooks 核心基石:深度解析 `useState` 的类型推断、泛型约束与空值安全
前端·typescript
YIAN1 小时前
吃透这 5 个核心点,你的 React+TS 代码直接上一个台阶
前端·react.js·typescript
Lear1 小时前
Vite+Vue3 模块自动导入实战:彻底告别繁琐的 import 语句
前端
Csvn2 小时前
📌 position: sticky 失效的 6 个经典坑:明明写了 sticky,为什么就是不吸顶?
前端
恋猫de小郭2 小时前
Flutter 的另外一种形态?社区 DartNative 要来了。
android·前端·flutter
秋天的一阵风2 小时前
🔥 Network 里那坨 "data:" 我真看吐了,自制开源 Chrome 插件,AI 流式调试直接开挂
前端·人工智能·后端
看到我请叫我铁锤2 小时前
vue编写web端在线预览文档
前端·javascript·vue.js
Ratten2 小时前
【Vben 解决】---- 跨页复选获取选择数据
前端