track函数:依赖收集的完整实现

0 阅读6分钟

在前面的文章中,我们分别探讨了 reactive 如何代理对象、ref 如何包装原始值,以及数组和集合的特殊处理。但这些都只是响应式系统的"骨架",真正让数据"活"起来的,是隐藏在背后的依赖收集机制。本文将深入剖析 track 函数的完整实现,揭开 Vue3 响应式核心的神秘面纱。

前言:为什么需要依赖收集?

当我们写下这样的代码时:

const state = reactive({ count: 0 });

effect(() => {
  console.log(`count的值是:${state.count}`);
});

state.count++; // 控制台自动打印新值

Vue3 是如何知道 effect 中的函数依赖于 state.count 的?又是如何在 state.count 变化时,精准地找到这个函数并重新执行?

答案就藏在依赖收集中。简单来说,依赖收集要做的事情是:

  • 在"读取"数据时,记录谁在读这个数据(收集依赖)。
  • 在"修改"数据时,通知所有在读它的人(触发更新)。

这就是 Vue3 响应式系统的核心:发布-订阅模式的一种实现。

targetMap:三层数据结构的设计

为什么需要三层结构?

要管理成千上万个响应式对象、每个对象的多个属性、每个属性的多个依赖,我们需要一个高效的数据结构。Vue3 选择了 WeakMap + Map + Set 的组合。我们先看伪代码的表示:

targetMap = WeakMap{
  target1: Map{
    key1: Set[effect1, effect2],
    key2: Set[effect3]
  },
  target2: Map{
    key1: Set[effect2, effect4]
  }
}

每一层的职责

第一层:targetMap(WeakMap)

const targetMap = new WeakMap();
targetMap.set(key, value);
  • Key: 响应式对象的原始对象(未经代理的原始值)。
  • Value: 一个 Map,存储该对象所有属性的依赖。

为什么用 WeakMap:当对象不再被使用时,WeakMap 不会阻止垃圾回收,避免内存泄漏。

第二层:depsMap(Map)

为每个对象的每个属性创建依赖Map:

let depsMap = targetMap.get(target);
if (!depsMap) {
  targetMap.set(target, (depsMap = new Map()));
}
  • Key: 对象的属性名(如 'count'、'name')
  • Value: 一个 Set,存储所有依赖该属性的副作用函数

第三层:dep(Set)

let dep = depsMap.get(key);
if (!dep) {
  depsMap.set(key, (dep = new Set()));
}
  • 数据结构: Set,确保同一个副作用函数不会被重复收集
  • 存储内容: 副作用函数的引用(即 ReactiveEffect 实例)

三层结构的可视化

依赖收集的三层结构 这种三层结构的设计,让 Vue3 能够:

  • 快速定位:通过 target 和 key 直接找到对应的依赖集合
  • 高效管理:Set 结构避免了重复收集
  • 自动回收:WeakMap 让未被引用的对象可以被垃圾回收

为什么需要记录 deps?

在依赖收集时,我们除了需要记录 activeEffect 外,每个 ReactiveEffect 实例还维护着一个 deps 数组,记录它被哪些依赖集合所包含。

class ReactiveEffect {
  deps = []; // 存储所有包含此effect的依赖集合
  
  // ...
}

function track(target, key) {
  if (!activeEffect) return;
  
  // ... 获取/创建 dep Set
  
  // 将当前effect添加到依赖集合
  dep.add(activeEffect);
  
  // 反向收集:让effect记住这个依赖集合
  activeEffect.deps.push(dep); // 关键:双向关联
}

这种双向关联的设计非常重要,它为后续的依赖清理和分支切换奠定了基础。

activeEffect 相关内容,在 effect函数的完整实现与追踪:深入Vue3响应式核心 一文中有详细介绍,本篇文章不再赘述!

避免重复收集

为什么需要避免重复?

effect(() => {
  console.log(state.count);
  console.log(state.count); // 同一个属性读了两次
});

如果不加处理,state.count 的依赖集合中会出现两个相同的 effect,当 count 变化时,effect 会被执行两次,造成性能浪费。

Set 天然去重

使用 Set 存储依赖,从根本上解决了重复收集的问题:

let dep = depsMap.get(key);
if (!dep) {
  dep = new Set();
  depsMap.set(key, dep);
}

// 即使多次调用,Set中也只会有一个activeEffect
dep.add(activeEffect);

更精细的控制:shouldTrack

除了 Set 的去重,Vue3 还通过 shouldTrack 标志进行更精细的控制,避免在某些情况下(如已经处于追踪过程中)重复收集。

手写实现:完整的 track 函数

基础版本

// 全局存储
const targetMap = new WeakMap();
let activeEffect = null;

/**
 * 依赖收集函数
 * @param {Object} target 原始对象
 * @param {string|symbol} key 属性名
 */
function track(target, key) {
  // 1. 如果没有正在运行的副作用,直接返回
  if (!activeEffect) return;
  
  // 2. 获取 target 对应的依赖 Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    // 第一次访问该对象,创建新的 Map
    targetMap.set(target, (depsMap = new Map()));
  }
  
  // 3. 获取 key 对应的依赖 Set
  let dep = depsMap.get(key);
  if (!dep) {
    // 第一次访问该属性,创建新的 Set
    depsMap.set(key, (dep = new Set()));
  }
  
  // 4. 检查是否已经收集过
  if (!dep.has(activeEffect)) {
    // 5. 将当前 effect 添加到依赖集合
    dep.add(activeEffect);
    
    // 6. 反向收集:让 effect 记住这个依赖集合
    activeEffect.deps.push(dep);
  }
}

完整版本(含边界处理)

实际 Vue3 源码中的 track 会更加严谨,包含各种边界情况的处理:

// 追踪操作类型枚举
const TrackOpTypes = {
  GET: 'get',
  HAS: 'has',
  ITERATE: 'iterate'
};

function track(target, type, key) {
  // 如果没有激活的effect,直接返回
  if (!activeEffect) return;
  
  // 获取target的依赖Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  
  // 根据操作类型和key构建依赖的标识
  let depKey = key;
  if (type === TrackOpTypes.ITERATE) {
    // 迭代操作(如 for...in)依赖的是对象的自身键
    depKey = ITERATE_KEY; // 特殊标识符
  }
  
  // 获取或创建依赖集合
  let dep = depsMap.get(depKey);
  if (!dep) {
    depsMap.set(depKey, (dep = new Set()));
  }
  
  // 检查是否需要追踪
  if (!isTracking()) return;
  
  // 收集依赖
  trackEffects(dep);
}

/**
 * 真正的依赖收集逻辑
 */
function trackEffects(dep) {
  // 检查是否已经收集过
  let shouldTrack = !dep.has(activeEffect);
  
  if (shouldTrack) {
    // 收集
    dep.add(activeEffect);
    
    // 反向收集
    activeEffect.deps.push(dep);
    
    // 调试用:记录一些额外信息
    if (__DEV__) {
      activeEffect._trackId++;
    }
  }
}

/**
 * 检查当前是否应该进行依赖收集
 */
function isTracking() {
  return activeEffect !== undefined && activeEffect.active;
}

在 reactive 中的调用时机

track 函数通常在 Proxyget 拦截器中被调用:

function createGetter(isReadonly = false) {
  return function get(target, key, receiver) {
    // 获取值
    const res = Reflect.get(target, key, receiver);
    
    // 如果不是只读,进行依赖收集
    if (!isReadonly) {
      // 关键:调用 track
      track(target, TrackOpTypes.GET, key);
    }
    
    // 如果值是对象,递归转换为响应式(懒加载)
    if (isObject(res)) {
      return reactive(res);
    }
    
    return res;
  };
}

在我之前的文章中,有的代码中也用到了 track 函数,但是并没有实现。

图解:依赖收集的全过程

流程图:从 effect 执行到依赖收集

依赖收集流程图

数据流向图:响应式数据的生命周期

数据流向图

结语

track 函数虽然只有短短几十行代码,却承载着 Vue3 响应式系统的核心逻辑。理解这些底层原理,不仅能帮助我们在开发中避免踩坑,更能让我们在性能优化时做出更明智的选择。下一篇文章,我们将深入探讨 trigger 函数,看看依赖收集完成后,Vue3 是如何精准地触发更新的。

对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!