在前面的文章中,我们分别探讨了 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 函数通常在 Proxy 的 get 拦截器中被调用:
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 是如何精准地触发更新的。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!