上文,我们详细分析了 Vue 的依赖收集,包括其核心数据结构(Link、Dep)和依赖收集的完整流程。而依赖收集中频繁提到的 ReactiveEffect,作为订阅(Subscriber)的具体实现,是整个响应式系统中另一个重要的组成部分。它不仅负责执行副作用函数,还要处理依赖的收集、清理以及更新的调度。本文将深入分析 Effect 系统的实现。
一、ReactiveEffect 类的设计与实现
1. 核心数据结构
// 源码位置: packages/reactivity/src/effect.ts
export interface ReactiveEffectRunner<T = any> {
(): T;
effect: ReactiveEffect;
}
export class ReactiveEffect<T = any> implements Subscriber {
// 依赖链表头部
deps?: Link = undefined;
// 依赖链表尾部
depsTail?: Link = undefined;
// 效果状态标志位
flags: EffectFlags = EffectFlags.ACTIVE | EffectFlags.TRACKING;
// 链表中的下一个订阅者
next?: Subscriber = undefined;
// 清理函数
cleanup?: () => void = undefined;
// 调度器
scheduler?: EffectScheduler = undefined;
// 停止时的回调
onStop?: () => void;
// 调试相关的回调
onTrack?: (event: DebuggerEvent) => void;
onTrigger?: (event: DebuggerEvent) => void;
constructor(public fn: () => T) {
// 如果存在活跃的作用域,将当前效果添加到作用域中
if (activeEffectScope && activeEffectScope.active) {
activeEffectScope.effects.push(this);
}
}
// 暂停当前 effect
pause(): void;
// 恢复当前 effect
resume(): void;
// 通知更新
notify(): void;
// 执行副作用函数
run(): T;
// 停止 effect
stop(): void;
// 触发更新
trigger(): void;
// 如果是脏的则运行
runIfDirty(): void;
// 获取脏状态
get dirty(): boolean;
}
2. 状态标志位系统
export enum EffectFlags {
ACTIVE = 1 << 0, // 效果是否处于活跃状态
RUNNING = 1 << 1, // 效果是否正在运行
TRACKING = 1 << 2, // 是否正在追踪依赖
NOTIFIED = 1 << 3, // 是否已被通知更新
DIRTY = 1 << 4, // 是否需要重新计算
ALLOW_RECURSE = 1 << 5, // 是否允许递归触发
PAUSED = 1 << 6, // 是否被暂停
}
3. 核心方法实现
run 方法 - 执行副作用函数
run(): T {
// 如果效果已停止,直接执行函数不收集依赖
if (!(this.flags & EffectFlags.ACTIVE)) {
return this.fn()
}
// 设置运行标记
this.flags |= EffectFlags.RUNNING
// 清理旧的依赖
cleanupEffect(this)
// 准备新的依赖收集
prepareDeps(this)
const prevEffect = activeSub
const prevShouldTrack = shouldTrack
activeSub = this
shouldTrack = true
try {
return this.fn()
} finally {
cleanupDeps(this)
activeSub = prevEffect
shouldTrack = prevShouldTrack
this.flags &= ~EffectFlags.RUNNING
}
}
run 方法是 effect 系统的核心执行方法,负责:
-
状态检查:
- 检查 effect 是否处于活跃状态
- 非活跃状态直接执行函数但不收集依赖
-
执行环境准备:
- 设置 RUNNING 标志
- 清理旧的依赖关系
- 准备新的依赖收集环境
-
上下文管理:
- 保存当前的全局 effect 状态
- 设置新的追踪状态
- 确保依赖收集的正确性
-
安全执行:
- 使用 try-finally 确保状态恢复
- 执行完成后清理依赖
- 恢复之前的全局状态
trigger 方法 - 触发更新
trigger(): void {
if (this.flags & EffectFlags.PAUSED) {
// 如果被暂停,加入暂停队列
pausedQueueEffects.add(this)
} else if (this.scheduler) {
// 有调度器则使用调度器执行
this.scheduler()
} else {
// 否则直接检查并运行
this.runIfDirty()
}
}
trigger 方法是响应式更新的触发入口,主要职责包括:
-
状态判断:
- 检查 effect 是否被暂停
- 判断是否存在自定义调度器
-
更新策略:
- 暂停状态:将 effect 加入暂停队列
- 有调度器:使用自定义调度器处理
- 默认情况:直接执行更新
-
性能优化:
- 支持更新暂停机制
- 通过调度器实现更新控制
- 脏检查避免不必要的更新
stop 方法 - 停止效果
stop(): void {
if (this.flags & EffectFlags.ACTIVE) {
// 移除所有依赖
for (let link = this.deps; link; link = link.nextDep) {
removeSub(link)
}
this.deps = this.depsTail = undefined
// 执行清理
cleanupEffect(this)
this.onStop && this.onStop()
// 清除活跃标记
this.flags &= ~EffectFlags.ACTIVE
}
}
stop 方法负责 effect 的停止和清理工作,包括:
-
状态检查:
- 确认 effect 当前是否处于活跃状态
- 避免重复停止操作
-
依赖清理:
- 遍历并移除所有依赖关系
- 重置依赖链表的头尾指针
- 执行清理函数
-
生命周期处理:
- 调用 onStop 回调
- 清除活跃状态标记
- 完成 effect 的生命周期
-
内存管理:
- 确保所有引用被正确清除
- 防止内存泄漏
- 支持垃圾回收
notify 方法 - 通知更新
notify(): void {
// 如果当前正在运行且不允许递归,则直接返回
if (
this.flags & EffectFlags.RUNNING &&
!(this.flags & EffectFlags.ALLOW_RECURSE)
) {
return
}
// 如果未被通知过,则加入批处理队列
if (!(this.flags & EffectFlags.NOTIFIED)) {
batch(this)
}
}
notify 方法是响应式系统中的关键方法,它负责:
-
递归控制:
- 检查当前 effect 是否正在运行
- 通过 ALLOW_RECURSE 标志控制是否允许递归更新
-
批量处理:
- 使用 NOTIFIED 标志避免重复通知
- 通过 batch 函数将更新加入批处理队列
-
更新触发:
- 最终通过批处理系统统一处理所有更新
- 确保更新的有序性和性能
pause 方法 - 暂停 effect
pause(): void {
this.flags |= EffectFlags.PAUSED
}
pause 方法实现了 effect 的暂停功能,主要职责包括:
-
状态管理:
- 设置 PAUSED 标志位
- 不影响其他状态标志
- 保持依赖关系不变
-
更新控制:
- 暂停后的更新会被加入暂停队列
- 不会立即触发更新
- 等待后续恢复
-
性能优化:
- 支持临时禁用某些更新
- 可用于批量更新场景
- 减少不必要的计算
resume 方法 - 恢复 effect
resume(): void {
if (this.flags & EffectFlags.PAUSED) {
this.flags &= ~EffectFlags.PAUSED
if (pausedQueueEffects.has(this)) {
pausedQueueEffects.delete(this)
this.trigger()
}
}
}
resume 方法负责恢复暂停的 effect,其功能包括:
-
状态恢复:
- 检查是否处于暂停状态
- 清除 PAUSED 标志位
- 保持其他状态不变
-
更新处理:
- 检查暂停队列中是否有待处理的更新
- 从暂停队列中移除
- 触发待处理的更新
-
执行优化:
- 只处理确实需要更新的 effect
- 避免重复触发
- 确保更新的连续性
-
生命周期管理:
- 与 pause 配对使用
- 支持暂停/恢复的完整周期
- 维护 effect 的正确状态
二、Watch 中的 Effect 应用
1. Watch 中的 Effect 创建和执行
// 源码位置: packages/reactivity/src/watch.ts
export function watch(
source: WatchSource | WatchSource[] | WatchEffect | object,
cb?: WatchCallback | null,
options: WatchOptions = EMPTY_OBJ
): WatchHandle {
// 1. 创建 getter 函数
let getter: () => any;
if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => traverse(source);
} else if (isFunction(source)) {
getter = source;
}
// 2. 创建 effect 实例
const effect = new ReactiveEffect(getter);
// 3. 配置调度器
effect.scheduler = scheduler
? () => scheduler(job, false)
: (job as EffectScheduler);
// 4. 设置清理函数
const cleanup = (effect.onStop = () => {
const cleanups = cleanupMap.get(effect);
if (cleanups) {
if (call) {
call(cleanups, WatchErrorCodes.WATCH_CLEANUP);
} else {
for (const cleanup of cleanups) cleanup();
}
cleanupMap.delete(effect);
}
});
// 5. 配置开发环境调试钩子
if (__DEV__) {
effect.onTrack = options.onTrack;
effect.onTrigger = options.onTrigger;
}
// 6. 首次执行
if (cb) {
if (immediate) {
job(true);
} else {
oldValue = effect.run();
}
} else if (scheduler) {
scheduler(job.bind(null, true), true);
} else {
effect.run();
}
// 7. 暴露控制方法
const watchHandle = {
effect,
pause: effect.pause.bind(effect),
resume: effect.resume.bind(effect),
stop: () => {
effect.stop();
cleanup();
},
};
return watchHandle;
}
// job 函数定义
const job = (immediateFirstRun?: boolean) => {
if (!(effect.flags & EffectFlags.ACTIVE)) {
return;
}
if (cb) {
// 获取新值
const newValue = effect.run();
// 判断是否需要触发回调
if (deep || immediateFirstRun || hasChanged(newValue, oldValue)) {
// 执行清理
if (cleanup) {
cleanup();
}
// 调用回调
cb(newValue, oldValue, onCleanup);
// 更新旧值
oldValue = newValue;
}
} else {
effect.run();
}
};
每个步骤的详细说明:
- 创建 getter 函数
// 源码位置: packages/reactivity/src/watch.ts
let getter: () => any;
if (isRef(source)) {
getter = () => source.value;
} else if (isReactive(source)) {
getter = () => traverse(source);
} else if (isFunction(source)) {
getter = source;
}
-
根据 source 类型创建不同的 getter:
- ref:返回
.value - reactive:使用
traverse递归访问 - function:直接使用函数本身
- ref:返回
-
getter 函数决定了依赖收集的范围
-
getter 函数就是用来获取你 watch 的那个值
- 创建 effect 实例
// 源码位置: packages/reactivity/src/watch.ts
const effect = new ReactiveEffect(getter)
// ReactiveEffect 构造函数
// 源码位置: packages/reactivity/src/effect.ts
constructor(public fn: () => T) {
if (activeEffectScope && activeEffectScope.active) {
activeEffectScope.effects.push(this)
}
}
- 使用 getter 创建 ReactiveEffect 实例
- 此时会自动关联到当前的 effectScope
- 为后续的依赖收集做准备
- 配置调度器
// 源码位置: packages/reactivity/src/watch.ts
effect.scheduler = scheduler
? () => scheduler(job, false)
: (job as EffectScheduler);
// job 函数定义
const job = (immediateFirstRun?: boolean) => {
if (!(effect.flags & EffectFlags.ACTIVE)) {
return;
}
if (cb) {
const newValue = effect.run();
if (deep || immediateFirstRun || hasChanged(newValue, oldValue)) {
cb(newValue, oldValue, onCleanup);
oldValue = newValue;
}
} else {
effect.run();
}
};
- 设置 effect.scheduler
- 可以是自定义调度器或默认的 job 函数
- 调度器负责控制更新时机和方式
- 设置清理函数
// 源码位置: packages/reactivity/src/watch.ts
const cleanup = (effect.onStop = () => {
const cleanups = cleanupMap.get(effect);
if (cleanups) {
if (call) {
call(cleanups, WatchErrorCodes.WATCH_CLEANUP);
} else {
for (const cleanup of cleanups) cleanup();
}
cleanupMap.delete(effect);
}
});
// 注册清理函数
const onCleanup = (fn: () => void) => {
let cleanups = cleanupMap.get(effect);
if (!cleanups) {
cleanupMap.set(effect, (cleanups = []));
}
cleanups.push(fn);
};
- 配置 effect.onStop 处理停止时的清理
- 管理 cleanupMap 中的清理函数
- 确保资源正确释放
- 配置开发环境调试钩子
// 源码位置: packages/reactivity/src/watch.ts
if (__DEV__) {
effect.onTrack = options.onTrack;
effect.onTrigger = options.onTrigger;
}
// 调试事件接口定义
// 源码位置: packages/reactivity/src/effect.ts
export interface DebuggerEvent {
effect: ReactiveEffect;
target: object;
type: TrackOpTypes | TriggerOpTypes;
key: any;
}
- 设置 onTrack 用于跟踪依赖收集
- 设置 onTrigger 用于跟踪依赖触发
- 仅在开发环境中生效
- 首次执行
// 源码位置: packages/reactivity/src/watch.ts
if (cb) {
if (immediate) {
job(true);
} else {
oldValue = effect.run();
}
} else if (scheduler) {
scheduler(job.bind(null, true), true);
} else {
effect.run();
}
-
根据配置选择执行策略:
- immediate + callback:直接执行 job
- 有 callback:运行 effect 获取旧值
- 有 scheduler:通过调度器执行
- 默认:直接运行 effect
- 暴露控制方法
// 源码位置: packages/reactivity/src/watch.ts
const watchHandle = {
effect,
pause: effect.pause.bind(effect),
resume: effect.resume.bind(effect),
stop: () => {
effect.stop();
cleanup();
},
};
return watchHandle;
- 暴露 effect 实例
- 绑定 pause/resume 方法
- 提供 stop 方法用于完全停止
2. Watch 中的 Effect 生命周期
通过分析 effect 在 watch 中的创建和执行,我们可以得出 effect 在 watch 中的生命周期分为以下几个阶段:
-
创建阶段
- 创建 ReactiveEffect 实例
- 配置调度器和清理函数
- 设置调试钩子(开发环境)
-
初始化阶段
- 根据配置执行首次运行
- 收集初始依赖
- 获取初始值(如果需要)
-
更新阶段
- 依赖变化触发 scheduler
- scheduler 执行 job 函数
- job 函数对比新旧值并触发回调
-
清理阶段
- 执行注册的清理函数
- 清除依赖关系
- 释放相关资源
-
控制阶段
- 支持通过 pause/resume 控制监听状态
- 可以通过 stop 完全停止监听
- 保持对监听器的完整控制能力
三、实际应用示例
1. 基础的响应式更新
const count = ref(0);
effect(() => {
console.log(count.value);
});
count.value++; // 触发更新
执行流程:
- 创建 effect 实例
- 首次运行收集依赖
- 修改值触发 trigger
- 通过调度系统执行更新
2. Watch 的实现
watch(
() => count.value,
(newVal, oldVal) => {
console.log(newVal, oldVal);
}
);
执行流程:
- 创建 getter 函数
- 创建 effect 实例
- 设置调度器和回调
- 首次运行收集依赖
- 后续更新时触发回调
四、总结
本文析了 Vue 的 Effect 系统,包括其核心数据结构(ReactiveEffect)和关键方法的实现。通过分析 watch 函数内 effect 的执行部分,我们了解了 effect 的详细执行流程和实际作用。下文来详细分析调度系统的具体实现。