上文,我们分析了渲染过程中响应式机制的建立。那么接下来我们就来具体分析响应式机制的具体实现细节,本文先分析依赖收集的具体实现细节。
一、整体设计
Vue 的依赖收集采用了精心设计的数据结构和算法,主要包括以下几个核心部分:
- Link 类:双向链表节点,连接依赖(Dep)和订阅者(Subscriber)
- Dep 类:管理依赖关系的核心类
- ReactiveEffect 类:响应式效果的实现,作为订阅者
- 依赖收集的数据结构:采用 WeakMap + Map + Set 的组合
二、核心数据结构
1. Link 类 - 依赖链接
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 类的设计亮点:
- 使用双向链表实现依赖和订阅者的多对多关系
- 通过版本号机制优化依赖清理
- 支持快速遍历和更新依赖关系
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 类的核心功能:
- 管理订阅者链表
- 提供依赖收集接口(track)
- 提供更新触发接口(trigger)
- 支持版本控制和计数统计
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();
}
数据结构设计的优点:
- 使用 WeakMap 避免内存泄漏
- Map 实现键值对的快速查找
- 支持任意类型的 key
- 层次化的结构便于管理和清理
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两个维度遍历数据。
这种双向链表结构的优势:
-
高效的依赖追踪:
- effect 可以通过 deps 快速找到所有依赖
- dep 可以通过 subs 快速找到所有订阅者
-
灵活的节点操作:
- 支持节点的快速插入和删除
- O(1) 时间复杂度的头尾操作
- 方便进行节点的移动和重排
-
优化的内存使用:
- 共享 Link 节点减少内存占用
- 无需额外的数组或集合
- 支持节点的复用
-
清晰的依赖关系:
- 双向链表清晰展示依赖关系
- 便于调试和依赖追踪
- 支持正向和反向遍历
三、依赖收集过程
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>
当组件渲染时,会执行以下流程:
- 渲染函数访问数据:
js
// 渲染函数中访问 title 属性
_ctx.title; // 这里的 _ctx 是组件实例的代理对象
- 触发代理的 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;
}
}
- 执行全局 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();
}
}
}
- 执行 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;
}
}
- 建立依赖关系:
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++;
}
这样,当渲染函数执行完成后:
title
属性已经建立了与当前渲染 effect 的依赖关系- 这个关系被存储在全局的
targetMap
中 - 通过双向链表可以快速找到互相的引用
- 当
title
发生变化时,就能通过这个依赖关系触发更新
数据结构示意图:
这种设计的优点:
- 层次化的数据结构使依赖关系清晰
- 双向链表支持快速操作和遍历
- WeakMap 的使用避免内存泄漏
- 完整的开发环境调试支持
3. 性能优化
- 版本控制:
js
// 每次响应式变化时递增
export let globalVersion = 0;
// Link 类中记录版本
this.version = dep.version;
- 批量处理:
js
export function batch(sub: Subscriber, isComputed = false): void {
startBatch();
try {
sub.notify();
} finally {
endBatch();
}
}
-
懒清理机制:
- 在 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. 清理策略
- 硬清理:完全移除依赖关系
- 软清理:保留结构但标记为无效
- 延迟清理:等待下次收集时再清理
五、总结
Vue 的依赖收集机制通过精心设计的数据结构和算法实现了:
-
高效的依赖管理:
- 双向链表实现快速操作
- 版本控制优化清理
- 分层结构便于管理
-
精确的更新触发:
- 准确找到相关依赖
- 批量处理提高性能
- 避免重复通知
-
完善的内存管理:
-
WeakMap 避免内存泄漏
-
及时清理无效依赖
-
支持垃圾回收
-