vue3源码解析:依赖收集

上文,我们分析了渲染过程中响应式机制的建立。那么接下来我们就来具体分析响应式机制的具体实现细节,本文先分析依赖收集的具体实现细节。

一、整体设计

Vue 的依赖收集采用了精心设计的数据结构和算法,主要包括以下几个核心部分:

  1. Link 类:双向链表节点,连接依赖(Dep)和订阅者(Subscriber)
  2. Dep 类:管理依赖关系的核心类
  3. ReactiveEffect 类:响应式效果的实现,作为订阅者
  4. 依赖收集的数据结构:采用 WeakMap + Map + Set 的组合

二、核心数据结构

js 复制代码
// 源码位置: packages/reactivity/src/dep.ts
export class Link {
  // 版本号,用于依赖清理
  version: number;

  // 双向链表指针
  nextDep?: Link; // 指向下一个依赖
  prevDep?: Link; // 指向前一个依赖
  nextSub?: Link; // 指向下一个订阅者
  prevSub?: Link; // 指向前一个订阅者
  prevActiveLink?: Link; // 指向前一个活动链接

  constructor(
    public sub: Subscriber, // 订阅者(effect)
    public dep: Dep // 依赖项
  ) {
    this.version = dep.version;
  }
}

Link 类的设计亮点:

  1. 使用双向链表实现依赖和订阅者的多对多关系
  2. 通过版本号机制优化依赖清理
  3. 支持快速遍历和更新依赖关系

2. Dep 类 - 依赖管理

js 复制代码
// 源码位置: packages/reactivity/src/dep.ts
export class Dep {
  version = 0; // 依赖版本号
  activeLink?: Link = undefined; // 当前活动的依赖链接
  subs?: Link = undefined; // 订阅者链表尾部
  subsHead?: Link; // 订阅者链表头部(仅开发环境)

  // 用于对象属性依赖清理
  map?: KeyToDepMap = undefined;
  key?: unknown = undefined;

  // 订阅者计数
  sc: number = 0;

  constructor(public computed?: ComputedRefImpl | undefined) {
    if (__DEV__) {
      this.subsHead = undefined;
    }
  }

  // 依赖收集
  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
    if (!shouldTrack || !activeSub) {
      return undefined;
    }

    // 创建新的依赖链接
    const link = new Link(activeSub, this);

    // 将链接添加到依赖和订阅者的链表中
    addSub(link);
    return link;
  }

  // 触发更新
  trigger(debugInfo?: DebuggerEventExtraInfo): void {
    if (this.subs) {
      startBatch();
      this.notify(debugInfo);
      endBatch();
    }
  }
}

Dep 类的核心功能:

  1. 管理订阅者链表
  2. 提供依赖收集接口(track)
  3. 提供更新触发接口(trigger)
  4. 支持版本控制和计数统计

3. 依赖收集的全局数据结构

js 复制代码
// 源码位置: packages/reactivity/src/dep.ts

// 类型定义
type KeyToDepMap = Map<any, Dep>;
const targetMap = new WeakMap<object, KeyToDepMap>();

// 依赖收集过程
export function track(target: object, type: TrackOpTypes, key: unknown): void {
  if (!shouldTrack || !activeSub) {
    return;
  }

  // 获取目标对象的依赖 Map
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }

  // 获取属性的依赖集合
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Dep()));
  }

  // 收集依赖
  dep.track();
}

数据结构设计的优点:

  1. 使用 WeakMap 避免内存泄漏
  2. Map 实现键值对的快速查找
  3. 支持任意类型的 key
  4. 层次化的结构便于管理和清理

4. 双向链表结构说明

js 复制代码
export class Link {
  // 双向链表指针
  nextDep?: Link; // 指向下一个依赖
  prevDep?: Link; // 指向前一个依赖
  nextSub?: Link; // 指向下一个订阅者
  prevSub?: Link; // 指向前一个订阅者
  prevActiveLink?: Link; // 指向前一个活动链接

  constructor(
    public sub: Subscriber, // 订阅者(effect)
    public dep: Dep // 依赖项
  ) {
    this.version = dep.version;
  }
}

Link节点把所有的发布者Dep和订阅者Sub链接成一张网,便于从deps何subs两个维度遍历数据。

这种双向链表结构的优势:

  1. 高效的依赖追踪

    • effect 可以通过 deps 快速找到所有依赖
    • dep 可以通过 subs 快速找到所有订阅者
  2. 灵活的节点操作

    • 支持节点的快速插入和删除
    • O(1) 时间复杂度的头尾操作
    • 方便进行节点的移动和重排
  3. 优化的内存使用

    • 共享 Link 节点减少内存占用
    • 无需额外的数组或集合
    • 支持节点的复用
  4. 清晰的依赖关系

    • 双向链表清晰展示依赖关系
    • 便于调试和依赖追踪
    • 支持正向和反向遍历

三、依赖收集过程

1. 收集触发点

依赖收集发生在访问响应式数据时:

js 复制代码
// 源码位置: packages/reactivity/src/baseHandlers.ts
class BaseReactiveHandler {
  get(target: object, key: unknown, receiver: object) {
    // ... 其他逻辑 ...

    // 追踪依赖
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key);
    }

    return result;
  }
}

2. 收集流程

以下面的组件为例,分析依赖收集的完整执行过程:

js 复制代码
<template>
  <div>{{ title }}</div>
</template>

<script>
export default {
  props: {
    title: String,
  },
};
</script>

当组件渲染时,会执行以下流程:

  1. 渲染函数访问数据
js 复制代码
// 渲染函数中访问 title 属性
_ctx.title; // 这里的 _ctx 是组件实例的代理对象
  1. 触发代理的 get 函数
js 复制代码
// 源码位置: packages/reactivity/src/baseHandlers.ts
class BaseReactiveHandler {
  get(target: object, key: unknown, receiver: object) {
    // 获取原始值
    const res = Reflect.get(target, key, receiver);

    // 追踪依赖
    if (!isReadonly) {
      track(target, TrackOpTypes.GET, key);
    }

    return res;
  }
}
  1. 执行全局 track 函数
js 复制代码
// 源码位置: packages/reactivity/src/dep.ts
export function track(target: object, type: TrackOpTypes, key: unknown): void {
  // 检查是否应该收集依赖
  if (shouldTrack && activeSub) {
    // 获取目标对象的依赖 Map
    let depsMap = targetMap.get(target);
    if (!depsMap) {
      // 如果不存在,创建一个新的 Map
      targetMap.set(target, (depsMap = new Map()));
    }

    // 获取属性的依赖对象
    let dep = depsMap.get(key);
    if (!dep) {
      // 如果不存在,创建一个新的 Dep 实例
      depsMap.set(key, (dep = new Dep()));
      // 保存反向引用,用于清理
      dep.map = depsMap;
      dep.key = key;
    }

    // 开发环境下收集更多的调试信息
    if (__DEV__) {
      dep.track({
        target,
        type,
        key,
      });
    } else {
      dep.track();
    }
  }
}
  1. 执行 Dep 实例的 track 方法
js 复制代码
// 源码位置: packages/reactivity/src/dep.ts
export class Dep {
  track(debugInfo?: DebuggerEventExtraInfo): Link | undefined {
    // 检查是否应该收集依赖
    if (!activeSub || !shouldTrack || activeSub === this.computed) {
      return;
    }

    // 尝试复用或创建新的 Link
    let link = this.activeLink;
    if (link === undefined || link.sub !== activeSub) {
      // 创建新的 Link 并建立链表关系
      link = this.activeLink = new Link(activeSub, this);

      // 将 link 添加到 activeSub 的依赖链表尾部
      if (!activeSub.deps) {
        activeSub.deps = activeSub.depsTail = link;
      } else {
        link.prevDep = activeSub.depsTail;
        activeSub.depsTail!.nextDep = link;
        activeSub.depsTail = link;
      }

      addSub(link);
    } else if (link.version === -1) {
      // 复用上次运行的 link - 已经是订阅者,只需同步版本
      link.version = this.version;

      // 如果这个 dep 有下一个节点,说明它不在尾部
      // 需要将它移动到尾部,确保 effect 的依赖列表按访问顺序排序
      if (link.nextDep) {
        // 1. 从当前位置断开
        const next = link.nextDep;
        next.prevDep = link.prevDep;
        if (link.prevDep) {
          link.prevDep.nextDep = next;
        }

        // 2. 移动到尾部
        link.prevDep = activeSub.depsTail;
        link.nextDep = undefined;
        activeSub.depsTail!.nextDep = link;
        activeSub.depsTail = link;

        // 如果是头节点,更新新的头节点
        if (activeSub.deps === link) {
          activeSub.deps = next;
        }
      }
    }

    // 开发环境的依赖追踪
    if (__DEV__ && activeSub.onTrack) {
      activeSub.onTrack(
        extend(
          {
            effect: activeSub,
          },
          debugInfo
        )
      );
    }

    return link;
  }
}
  1. 建立依赖关系
js 复制代码
// 源码位置: packages/reactivity/src/dep.ts
function addSub(link: Link): void {
  const { sub, dep } = link;

  // 1. 添加到订阅者的依赖链表
  if (!sub.deps) {
    // 如果是第一个依赖,设置头尾指针
    sub.deps = sub.depsTail = link;
  } else {
    // 否则添加到链表尾部
    link.prevDep = sub.depsTail;
    sub.depsTail!.nextDep = link;
    sub.depsTail = link;
  }

  // 2. 添加到依赖的订阅者链表
  if (!dep.subs) {
    // 如果是第一个订阅者
    dep.subs = link;
    if (__DEV__) {
      dep.subsHead = link;
    }
  } else {
    // 否则添加到链表头部
    link.prevSub = dep.subs;
    dep.subs.nextSub = link;
    dep.subs = link;
  }

  // 3. 更新订阅者计数
  dep.sc++;
}

这样,当渲染函数执行完成后:

  1. title 属性已经建立了与当前渲染 effect 的依赖关系
  2. 这个关系被存储在全局的 targetMap
  3. 通过双向链表可以快速找到互相的引用
  4. title 发生变化时,就能通过这个依赖关系触发更新

数据结构示意图:

这种设计的优点:

  1. 层次化的数据结构使依赖关系清晰
  2. 双向链表支持快速操作和遍历
  3. WeakMap 的使用避免内存泄漏
  4. 完整的开发环境调试支持

3. 性能优化

  1. 版本控制
js 复制代码
// 每次响应式变化时递增
export let globalVersion = 0;

// Link 类中记录版本
this.version = dep.version;
  1. 批量处理
js 复制代码
export function batch(sub: Subscriber, isComputed = false): void {
  startBatch();
  try {
    sub.notify();
  } finally {
    endBatch();
  }
}
  1. 懒清理机制

    • 在 effect 运行前重置所有依赖的版本号
    • 运行时同步依赖版本
    • 运行后清理版本号为 -1 的依赖

四、依赖清理机制

1. 清理时机

js 复制代码
// 源码位置: packages/reactivity/src/effect.ts
function cleanupEffect(e: ReactiveEffect) {
  // 清理旧的依赖关系
  let link = e.deps;
  while (link) {
    const next = link.nextDep;
    removeDep(link);
    link = next;
  }
  e.deps = e.depsTail = undefined;
}

2. 清理策略

  1. 硬清理:完全移除依赖关系
  2. 软清理:保留结构但标记为无效
  3. 延迟清理:等待下次收集时再清理

五、总结

Vue 的依赖收集机制通过精心设计的数据结构和算法实现了:

  1. 高效的依赖管理

    • 双向链表实现快速操作
    • 版本控制优化清理
    • 分层结构便于管理
  2. 精确的更新触发

    • 准确找到相关依赖
    • 批量处理提高性能
    • 避免重复通知
  3. 完善的内存管理

    • WeakMap 避免内存泄漏

    • 及时清理无效依赖

    • 支持垃圾回收

相关推荐
前端小趴菜055 分钟前
react状态管理库 - zustand
前端·react.js·前端框架
Jerry Lau33 分钟前
go go go 出发咯 - go web开发入门系列(二) Gin 框架实战指南
前端·golang·gin
我命由我123451 小时前
前端开发问题:SyntaxError: “undefined“ is not valid JSON
开发语言·前端·javascript·vue.js·json·ecmascript·js
0wioiw01 小时前
Flutter基础(前端教程③-跳转)
前端·flutter
落笔画忧愁e1 小时前
扣子Coze纯前端部署多Agents
前端
海天胜景1 小时前
vue3 当前页面方法暴露
前端·javascript·vue.js
GISer_Jing1 小时前
前端面试常考题目详解
前端·javascript
Boilermaker19922 小时前
【Java EE】SpringIoC
前端·数据库·spring
中微子2 小时前
JavaScript 防抖与节流:从原理到实践的完整指南
前端·javascript
天天向上10243 小时前
Vue 配置打包后可编辑的变量
前端·javascript·vue.js