手写 Vue 源码 === 完善依赖追踪与触发更新

7 阅读5分钟

手写 Vue 源码 === 完善依赖追踪与触发更新

目录

[TOC]

在上一篇文章中,我们介绍了Vue3响应式系统的基本原理和 activeEffect 的作用。现在,我们将深入探讨完善后的依赖追踪和触发更新机制,特别是 track、 trigger、 trackEffects 和 triggerEffects 函数的实现,以及 ReactiveEffect 类中新增的属性。

class ReactiveEffect {
  _trackId = 0; // 当前的 effect 执行了几次
  deps = []; // 当前的 effect 依赖了哪些属性
  _depsLength = 0; // 当前的 effect 依赖的属性有多少个
  
  public active = true; //默认是响应式的
  constructor(public fn, public scheduler) {}
  // ...
}

这些新增的属性有重要的作用:

  • _trackId:记录 effect 执行的次数,用于优化依赖收集
  • deps:存储当前 effect 依赖的所有属性的依赖集合
  • _depsLength:记录依赖的属性数量,避免频繁计算数组长度

依赖收集的完整实现

// 存储依赖收集的关系
const targetMap = new WeakMap();
 
export const createDep = (cleanUp, key) => {
  const dep = new Map() as any; //创建的收集器还是一个map
  dep.cleanUp = cleanUp; //清理方法
  dep.name = key; //收集器名称
  return dep;
};
 
export function track(target, key) {
  if (activeEffect) {
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      targetMap.set(target, (depsMap = new Map()));
    }
    let dep = depsMap.get(key);
    if (!dep) {
      depsMap.set(key, (dep = createDep(() => depsMap.delete(key), key)));
    }
 
    trackEffects(activeEffect, dep);
    console.log(targetMap);
  }
}

与之前相比,有几个重要的变化:

  • 依赖集合从 Set 变成了 Map,这允许我们存储更多信息
  • 使用 createDep 函数创建依赖集合,增加了清理方法和名称标识
  • 调用 trackEffects 函数建立具体的依赖关系

}

track 函数的执行过程:

  • 检查是否有 activeEffect,有则继续
  • 尝试从 targetMap 获取目标对象的依赖映射 depsMap
  • 如果没有,创建一个新的 Map 并存入 targetMap
  • 尝试从 depsMap 获取属性的依赖集合 dep
  • 如果没有,创建一个新的依赖集合并存入 depsMap
  • 调用 trackEffects(activeEffect, dep) 建立具体的依赖关系

这里的 createDep 函数创建了一个特殊的 Map 作为依赖集合,它包含:

一个清理方法 cleanUp,用于在不需要时删除依赖

一个名称标识 name,用于调试和识别

trackEffects:建立双向依赖关系

export function trackEffects(effect, dep) {
  // 这个属性中有 哪些 effect
  dep.set(effect, effect._trackId);
  // 我还想知道 effect 中有哪些收集器 dep
  effect.deps[effect._depsLength++] = dep;
  console.log(effect.deps);
}

将 effect 添加到 dep 集合中: dep.set(effect, effect._trackId)

  • 使用 Map 的 set 方法,键是 effect,值是 effect 的执行次数
  • 这记录了"这个属性被哪些 effect 依赖"

将 dep 添加到 effect.deps 数组中: effect.deps[effect._depsLength++] = dep

  • 使用数组存储 dep,并增加 _depsLength 计数
  • 这记录了"这个 effect 依赖了哪些属性"

这种双向关系非常重要,它使得:

  • 当属性变化时,可以找到所有依赖该属性的 effect
  • 当 effect 需要清理时,可以找到所有与之相关的依赖集合

触发更新的完整实现

当响应式对象的属性发生变化时,需要触发相关的 effect 重新执行:

export function trigger(target, key, value, oldValue) {
  let depsMap = targetMap.get(target);
  if (!depsMap) return; //找不到对象,直接 return 即可
  let dep = depsMap.get(key);
  if (dep) {
    //修改属性对应的 effect
    triggerEffects(dep);
  }
}

trigger 函数找到与目标对象和属性相关的依赖集合,然后调用 triggerEffects 函数:

export function triggerEffects(dep) {
  for (let effect of dep.keys()) {
    if (effect.scheduler) {
      effect.scheduler();
    }
  }
}

triggerEffects 函数遍历依赖集合中的所有 effect,如果 effect 有调度器(scheduler),就调用调度器。这里的调度器通常是一个函数,它会调用 effect.run() 方法重新执行 effect。

  1. 设置   activeEffect 为当前 effect
  2. 执行回调函数,再次访问 state.name
  3. 由于 state.name 已经变成了 'li',所以会打印 "Effect running: li"
  4. 更新 DOM: document.body.innerHTML = 'li'
​
const state = reactive({ name: 'zhang', age: 18 });
 
effect(() => {
  console.log('Effect running:', state.name);
  document.body.innerHTML = state.name;
});
 
// 稍后修改数据
setTimeout(() => {
  state.name = 'li';
}, 1000);
 
​

完整的响应式流程

现在,我们可以描述一个完整的响应式流程:

  • 创建响应式对象:const state = reactive({ name: 'zhang' })
  • 创建 effect:effect(() => console.log(state.name))
  • effect 执行,设置 activeEffect
  • 访问 state.name,触发 Proxy 的 get 捕获器
  • get 捕获器调用 track(state, 'name')
  • track 函数创建依赖映射,并调用 trackEffects
  • trackEffects 建立双向依赖关系
  • 当 state.name 变化时,触发 Proxy 的 set 捕获器
  • set 捕获器调用 trigger(state, 'name', newValue, oldValue)
  • trigger 函数找到相关依赖,并调用 triggerEffects
  • triggerEffects 遍历依赖集合,调用 effect 的调度器
  • 调度器重新执行 effect,完成响应式更新

为什么使用 Map 而不是 Set?

在新的实现中,依赖集合从 Set 变成了 Map ,这是因为:

  1. Map  可以存储键值对,我们可以用 effect 作为键,存储额外的信息
  2. 这里使用 effect._trackId 作为值,可能用于跟踪 effect 的执行次数
  3. Map  提供了更多的操作方法,如 keys()values()

整个依赖收集系统的数据结构可以这样理解:

targetMap (WeakMap):
  - key: 原始对象 { name: 'zhang', age: 18 }
  - value: depsMap (Map)
    - key: 'name'
    - value: dep (Map)
      - key: effectA
      - value: effectA._trackId
 
      effectA.deps = [dep1, dep2, ...] // 记录 effectA 依赖的所有属性

总结

Vue3 响应式系统通过精心设计的数据结构和算法,实现了高效的依赖追踪和更新触发:

  1. 响应式对象 :使用 Proxy 拦截属性访问和修改
  2. 依赖收集 :通过全局  activeEffect 和多层映射表建立对象、属性与 effect 的关系
  3. 双向依赖 :不仅记录属性依赖了哪些 effect,还记录 effect 依赖了哪些属性
  4. 精确更新 :只有当依赖的属性发生变化时,才会触发相关的 effect 重新执行
  5. 调度机制 :通过调度器控制 effect 的执行时机和方式

这种设计使得 Vue3 的响应式系统既高效又灵活,能够精确地追踪依赖关系并在数据变化时触发必要的更新,同时还支持嵌套 effect、计算属性等高级特性。