FORMA

依赖收集与派发更新机制

说明 Vue 3 中 track / triggertargetMap 的工作方式。前置:v2-v3 差异

一、依赖收集(Track)

依赖收集的目的是建立 数据 → 副作用函数(effect) 的映射关系,使得当数据变化时,能够自动执行相关的副作用(如组件渲染、计算属性、watch)。

1. 触发时机:在 getter 拦截中

Vue 3 使用 Proxy 拦截对象属性的读取操作(get 陷阱)。当组件渲染、computedwatch 运行时,会读取响应式数据,从而触发 get 拦截器,track 函数被调用。

js
// 简化逻辑
function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    // 依赖收集
    track(target, key);
    // 如果是对象,递归代理(懒代理)
    if (isObject(res)) return reactive(res);
    return res;
  };
}

2. 映射关系的存储结构:targetMap

Vue 3 维护了一个全局的 WeakMap,名为 targetMap。其结构为:

  • :原始对象(target
  • :一个 Map,键为属性名(key),值为一个 Set 集合,存放依赖该属性的所有副作用函数(effect)。
text
targetMap (WeakMap)
  ├─ target1 (obj) → Map
  │   ├─ key1 → Set[effect1, effect2, ...]
  │   └─ key2 → Set[effect3, ...]
  └─ target2 → Map...

3. 当前激活的副作用:activeEffect

为了知道当前正在执行的副作用是哪一个,Vue 3 使用了一个全局变量 activeEffect。当执行 effect 函数时,会先将自身赋值给 activeEffect,然后执行回调(回调中读取数据,触发 track),最后重置。

js
function effect(fn) {
  const _effect = new ReactiveEffect(fn);
  _effect.run(); // 首次执行,触发依赖收集
}

class ReactiveEffect {
  constructor(fn) {
    this.fn = fn;
  }
  run() {
    activeEffect = this;
    return this.fn();
  }
}

track 中,如果存在 activeEffect,就将它添加到对应属性的依赖集合中。

二、派发更新(Trigger)

当响应式数据被修改时(触发 set 拦截器),Vue 3 会找出依赖该数据的所有副作用函数,并重新执行它们。

1. 触发时机:在 setter 拦截中

js
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    const result = Reflect.set(target, key, value, receiver);
    // 如果值真的变化了,触发更新
    if (oldValue !== value) {
      trigger(target, key, value, oldValue);
    }
    return result;
  };
}

2. 异步更新队列与 nextTick

Vue 3 并不会同步立即执行每个副作用,而是将需要执行的副作用缓冲到一个队列中,然后通过微任务(Promise.thenqueueMicrotask)在下一轮事件循环统一执行。这称为 批量异步更新

  • 相同的副作用只会被加入队列一次(去重)。
  • 通过 nextTick 可以在 DOM 更新后执行回调。
  • 底层实现:queueFlush 函数将 flushJobs 放入微任务队列。
js
let isFlushPending = false;
const pendingPreFlushCbs = [];
function queueJob(job) {
  if (!pendingPreFlushCbs.includes(job)) {
    pendingPreFlushCbs.push(job);
    if (!isFlushPending) {
      isFlushPending = true;
      Promise.resolve().then(flushJobs);
    }
  }
}

flushJobs 会将队列中的所有副作用按顺序执行,完成之后再清空。

3. trigger 的查找过程

trigger 根据被修改的 targetkey,从 targetMap 中取出对应的依赖集合,遍历执行每个 effect。如果 effect 配置了 scheduler(调度器),则会调用调度器(通常是放入队列)。

三、refreactive 的底层实现差异

1. reactive

  • 适用对象:仅能代理对象类型(Object、Array、Map、Set 等)。
  • 实现:直接基于 Proxy 包装原对象,返回一个代理对象。代理对象和原对象保持引用关系,所有属性访问都被拦截。
  • 缺陷:不能直接处理原始值(numberstringboolean 等),因为 Proxy 无法代理原始值。

2. ref

  • 适用对象:可以包装任何类型,尤其是原始值
  • 实现:创建一个包含 value 属性的普通对象,并为该属性添加响应式逻辑。
    • 如果传入的是对象,内部会调用 reactive 将该对象转换为响应式代理。
    • 如果传入的是原始值,则通过包装对象 .value 的 getter/setter 实现响应式(内部仍使用类似 trackRefValue / triggerRefValue 的独立依赖管理)。
  • 访问方式:必须通过 .value 读取或修改(模板中自动解包,但脚本中需要 .value)。

简化版 ref 实现

js
function ref(value) {
  const r = {
    _value: value,
    get value() {
      track(r, "value"); // 依赖收集
      return this._value;
    },
    set value(newVal) {
      if (newVal !== this._value) {
        this._value = newVal;
        trigger(r, "value"); // 派发更新
      }
    },
  };
  if (isObject(value)) r._value = reactive(value);
  return r;
}

3. 核心差异总结

特性reactiveref
输入类型仅对象(包括数组、集合)任意类型(原始值 + 对象)
底层代理Proxy普通对象 + getter/setter(对象时内嵌 reactive)
访问方式直接访问属性通过 .value 访问
解构响应性会丢失,需 toRefs直接解构 .value 也会丢失,一般不解构
模板中的行为直接使用自动解包(直接写变量名,无需 .value
典型使用场景复杂状态对象、表单数据单个原始值、需要重新赋值的场景

四、工作流示例:组件渲染

  1. 组件初始化:创建 setup() 中定义的响应式数据(ref/reactive)。
  2. 首次渲染:执行 render 函数(副作用),读取响应式数据 → 触发 gettertrack 收集当前渲染副作用作为依赖。
  3. 数据变更:用户交互或异步操作修改数据 → 触发 settertrigger 将渲染副作用放入异步队列。
  4. 微任务执行flushJobs 执行队列,重新调用 render 函数,更新虚拟 DOM 并 patch 到真实 DOM。
  5. nextTick 回调在 DOM 更新后执行。

五、性能相关的设计考量

  • 懒递归reactive 只在读取嵌套对象时才进行深层代理,避免初始化开销。
  • 依赖清理:每次重新执行副作用前,会清除旧依赖,避免无效依赖积累(通过 effectdeps 数组)。
  • 调度器:允许自定义更新时机(如 watchflush: 'sync''post')。
  • 异步队列去重:同一个副作用多次触发只会执行一次。

理解这些机制,有助于写出更高性能的 Vue 应用,也能在遇到响应式相关 BUG(如数据改变但视图未更新)时快速定位问题。

参考文献

以下链接在编写时均可正常访问:

资料说明
Vue:深入响应式官方说明
Vue:refref API
Vue:reactivereactive API

Series

reactivity

2 / 3