1. effect是什么
Effect 是 Vue 3 响应式系统的核心 API,作用是创建一个响应式副作用 (Reactive Effect)。它在响应式系统中充当"发动机"的角色:
响应式追踪器:自动追踪函数内部访问的所有响应式依赖
自动执行器:当依赖的响应式数据变化时,自动重新运行函数
依赖管理者:管理副作用函数与响应式数据的绑定关系
2. 用法
Vue 3 的源代码设计将 effect 定位为一个底层内部 API。它被实现于 @vue/reactivity 这个独立的响应式核心包中,但在整合到主 vue 包时,并未将其作为公共 API 对外暴露 。这意味着,如果你在项目中通过 import { effect } from 'vue' 的方式引入,将会得到一个错误,因为 vue 包的默认导出中不包含 effect 函数。
这种设计是 Vue 团队有意为之的,旨在"降低开发者的心智负担"。effect 作为响应式副作用的底层抽象,其直接使用涉及依赖收集、执行调度、清理机制等复杂概念,如果广泛暴露,可能会被误用,导致无限循环(例如在 effect 内部修改其依赖的数据)或性能问题。因此,Vue 提供了更高层、更易用的 API(如 watch、watchEffect、computed)来封装 effect 的能力,这些才是推荐开发者日常使用的工具。
这部分的用法仅展示与 reactive 使用内部源码的执行,如 ref 与 computed 等待后续内容解锁(后面解析到 ref 与 computed 会具体去讲)
let dummy = 0
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))
expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
3. effct的执行流程(核心步骤)
整个 effect 的执行围绕track(依赖收集) 和**trigger(触发更新)**展开:
步骤1:初始化阶段
步骤2:执行期间的依赖收集(Track)
步骤3:更新触发阶段(Trigger)
步骤4:重新执行前的清理
步骤5:执行完成
4. 源码解析
这里以用法中的代码为参照,讲一下代码关键部分和经过部分,其他的内容等待后续解锁(后面再具体去说)
effect 函数内部运行过程
- 建新的
ReactiveEffect实例 - 将新创建的
ReactiveEffect实例手动执行内部的run方法进行依赖收集
路径: packages\reactivity\src\effect.ts
/**
* 创建一个响应式副作用函数
* @param fn - 要执行的响应式函数
* @param options - 可选配置,包含调度器和停止回调
* @returns ReactiveEffectRunner,用于手动控制 effect
*/
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions,
): ReactiveEffectRunner<T> {
// 如果传入的 fn 已经是 ReactiveEffectRunner(通过 effect() 返回的对象)
// 则提取其内部的 ReactiveEffect 实例的 fn 函数
// 这允许 effect() 接受另一个 effect 返回的 runner 作为参数
if ((fn as ReactiveEffectRunner).effect instanceof ReactiveEffect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 创建新的 ReactiveEffect 实例 (重点)
const e = new ReactiveEffect(fn)
// ... 此处省略了一部分代码
// 立即执行一次 effect,进行依赖收集
// 如果执行过程中抛出错误,先停止 effect 再抛出
try {
e.run() // 手动一次effect,运行 fn 进行依赖收集,让响应式数据与effect绑定
} catch (err) {
e.stop()
throw err
}
// 创建一个 runner 函数,绑定到 ReactiveEffect 的 run 方法
const runner = e.run.bind(e) as ReactiveEffectRunner
// 将 ReactiveEffect 实例挂载到 runner 上,方便外部通过 runner.effect 访问
runner.effect = e
return runner
}
/**
* 清理函数
* 执行所有注册的清理回调,用于:
* 1. effect 重新执行前清理上一次的资源
* 2. effect 停止时清理资源
*
* @param sub - 实现了 cleanups 和 cleanupsLength 的 ReactiveNode(通常是 ReactiveEffect)
*/
export function cleanup(
sub: ReactiveNode & { cleanups: (() => void)[]; cleanupsLength: number },
): void {
// 获取清理函数的数量
const l = sub.cleanupsLength
// 如果有清理函数需要执行
if (l) {
// 遍历执行所有清理函数
for (let i = 0; i < l; i++) {
sub.cleanups[i]()
}
// 重置清理函数数量为 0
sub.cleanupsLength = 0
}
}
在看 ReactiveEffect 前, 需要知道 依赖收集是什么
概念: 依赖收集是指 Vue.js 用于跟踪组件与其所依赖的数据之间关系的过程 。当数据被读取时,系统会记录下"谁"正在使用这个数据;当数据被修改时,系统就能精确地通知到所有"依赖"于此数据的部分进行更新。其根本目标是实现数据驱动视图的自动化,让开发者无需手动管理数据与视图的同步,从而提升开发效率和应用的性能。
依赖收集的实现本质上是观察者模式在响应式系统中的具体应用
当用法中的代码进行到这里的时候, 首先调用 ReactiveEffect 构造函数,创建 ReactiveEffect 实例对象,
紧接着会执行 run 方法, 执行到 startTracking 为依赖收集做准备,
后面会手动调用,用户提供的 fn ,在执行 fn 时若访问了响应式对象,紧接着就会触发响应式对象的 get 方法
位置: packages\reactivity\src\effect.ts
/**
* 响应式副作用类
* 负责执行响应式函数并管理依赖追踪和触发更新
*/
export class ReactiveEffect<T = any>
implements ReactiveEffectOptions, ReactiveNode
{
/**
* 依赖链表头指针 - 指向该 effect 依赖的第一个 dep
*/
deps: Link | undefined = undefined
/**
* 依赖链表尾指针 - 指向该 effect 依赖的最后一个 dep
*/
depsTail: Link | undefined = undefined
/**
* 订阅者链表头指针 - 指向订阅该 effect 的第一个订阅者
*/
subs: Link | undefined = undefined
/**
* 订阅者链表尾指针 - 指向订阅该 effect 的最后一个订阅者
*/
subsTail: Link | undefined = undefined
/**
* 状态标志位
* 初始值: Watching | Dirty
* - Watching: 表示正在追踪依赖
* - Dirty: 表示数据已变化,需要重新执行
* - STOP: 表示已停止
* - PAUSED: 表示已暂停
* - Recursed: 表示正在递归执行
* - ALLOW_RECURSE: 允许递归
*/
// 默认标志位为 Watching | Dirty
flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty
/**
* 清理函数数组 - 存储 effect 停止时需要执行的清理回调
* @internal
*/
cleanups: (() => void)[] = []
/**
* 清理函数数量
* @internal
*/
cleanupsLength = 0
// dev only - 调试选项:追踪时的回调
onTrack?: (event: DebuggerEvent) => void
// dev only - 调试选项:触发时的回调
onTrigger?: (event: DebuggerEvent) => void
// 用户传入的响应式函数
// @ts-expect-error
fn(): T {}
/**
* 构造函数
* @param fn - 可选的响应式函数
*/
constructor(fn?: () => T) {
// 如果传入了函数,则保存到 fn 属性
if (fn !== undefined) {
this.fn = fn
}
// 此处省略一部分代码 ...
}
/**
* 判断 effect 是否处于活跃状态
* 通过检查 STOP 标志位来判断
*/
get active(): boolean {
return !(this.flags & EffectFlags.STOP)
}
/**
* 暂停 effect - 设置 PAUSED 标志位
* 暂停后,effect 不会被自动触发更新
*/
pause(): void {
this.flags |= EffectFlags.PAUSED
}
/**
* 恢复 effect - 清除 PAUSED 标志位
* 如果有待处理的更新(Dirty 或 Pending),则立即触发
*/
resume(): void {
// 清除 PAUSED 标志位
const flags = (this.flags &= ~EffectFlags.PAUSED)
// 如果有未处理的变更,则触发更新
if (flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) {
this.notify()
}
}
/**
* 通知 effect 执行
* 触发依赖变化后的回调
*/
notify(): void {
// 只有在未暂停且数据发生变化时才执行
if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
this.run()
}
}
/**
* 执行 effect 函数
* 1. 如果已停止,直接执行 fn(不追踪依赖)
* 2. 否则清理旧的依赖,追踪新的依赖,执行 fn
* @returns fn 的返回值
*/
run(): T {
// 如果 effect 已停止,以非追踪模式执行
if (!this.active) { // 这里会触发ReactiveEffect实例active的get方法
return this.fn()
}
// 清理旧的依赖追踪
cleanup(this)
// 开始追踪依赖,保存之前的订阅者
const prevSub = startTracking(this)
try {
// 执行用户函数,返回其返回值
return this.fn()
} finally {
// 结束追踪依赖
endTracking(this, prevSub)
const flags = this.flags
// 检查是否需要递归触发
// 如果同时设置了 Recursed 和 ALLOW_RECURSE 标志
if (
(flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
(ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
) {
// 清除 Recursed 标志,然后触发更新
this.flags = flags & ~ReactiveFlags.Recursed
this.notify()
}
}
}
/**
* 停止 effect
* 1. 设置 STOP 标志位
* 2. 从所有依赖的 dep 中解绑
* 3. 从所有订阅者中解绑
* 4. 执行清理函数
*/
stop(): void {
// 如果已经停止,则直接返回
if (!this.active) {
return
}
// 设置 STOP 标志位
this.flags = EffectFlags.STOP
// 从所有依赖中解绑
let dep = this.deps
while (dep !== undefined) {
dep = unlink(dep, this)
}
// 从所有订阅者中解绑
const sub = this.subs
if (sub !== undefined) {
unlink(sub)
}
// 执行清理函数
cleanup(this)
}
/**
* 检查 effect 是否需要重新执行
* @returns true 表示数据已变化需要执行,false 表示不需要
*/
get dirty(): boolean {
const flags = this.flags
// 如果已经标记为 Dirty,直接返回 true
if (flags & ReactiveFlags.Dirty) {
return true
}
// 如果有待处理的变更,检查是否真的需要执行
if (flags & ReactiveFlags.Pending) {
if (checkDirty(this.deps!, this)) {
// 标记为 Dirty 并返回 true
this.flags = flags | ReactiveFlags.Dirty
return true
} else {
// 清除 Pending 标志
this.flags = flags & ~ReactiveFlags.Pending
}
}
// 没有变化,返回 false
return false
}
}
startTracking 是 effect 运行之前的准备工作,为依赖收集做准备,保存之前活跃的 sub ,并将活跃 sub 设置为当前 sub
位置: packages\reactivity\src\system.ts
/**
* 开始依赖追踪
* 在 effect 执行前调用,初始化追踪状态
*
* 1. 递增全局版本号 - 用于判断依赖链接是否最新
* 2. 清空依赖链表 - 准备建立新的依赖关系
* 3. 清除旧的状态标志 - 重置为初始追踪状态
* 4. 设置当前 effect 为活跃订阅者
*
* @param sub - 正在追踪的 ReactiveNode(通常是 ReactiveEffect)
* @returns 之前活跃的订阅者(用于在 endTracking 时恢复)
*/
export function startTracking(sub: ReactiveNode): ReactiveNode | undefined {
// 1. 递增全局版本号
// 每次开始追踪都使用新的版本号,用于判断链接是否为最新的
++globalVersion
// 2. 清空依赖链表尾部
// 重新建立依赖关系,从空链表开始
sub.depsTail = undefined
// 3. 重置状态标志
// 清除之前可能存在的状态:
// - Recursed: 递归标志
// - Dirty: 脏数据标志
// - Pending: 待处理标志
// 然后设置 RecursedCheck 标志,表示正在追踪
sub.flags =
(sub.flags &
~(ReactiveFlags.Recursed | ReactiveFlags.Dirty | ReactiveFlags.Pending)) |
ReactiveFlags.RecursedCheck
// 4. 设置当前 effect 为活跃订阅者
// 并返回之前的活跃订阅者(用于嵌套追踪时恢复)
return setActiveSub(sub)
}
/**
* 设置当前活跃的订阅者
* 这是一个重要的全局状态,用于追踪当前正在执行哪个 effect
*
* 使用 try-finally 确保即使发生异常,也能正确更新 activeSub
*
* @param sub - 要设置为活跃的 ReactiveNode(通常是 ReactiveEffect),传入 undefined 表示清除
* @returns 之前活跃的订阅者
*
* @example
* // 在 startTracking 中
* const prevSub = setActiveSub(effect) // 设置新的,返回旧的
*
* // 在 endTracking 中
* setActiveSub(prevSub) // 恢复之前的
*/
export function setActiveSub(sub?: ReactiveNode): ReactiveNode | undefined {
try {
// 先返回当前的 activeSub
return activeSub
} finally {
// 然后设置新的 activeSub
activeSub = sub
}
}
当运行到 ReactiveEffect.run 内调用 用户 fn 若访问了 reactive 代理后的对象, 就会触发响应式的 get 方法, 在 get 触发 track 进行依赖收集
位置: packages\reactivity\src\reactive.ts
class BaseReactiveHandler implements ProxyHandler<Target> {
constructor(
// 是否只只读对象
protected readonly _isReadonly = false,
// 是否为浅响应式对象
protected readonly _isShallow = false,
) {}
/**
* 处理代理的 get 操作
* @param target 目标对象
* @param key 访问的属性名
* @param receiver 目标对象的原型或接收者对象
* @returns 访问结果
*/
get(target: Target, key: string | symbol, receiver: object): any {
// 此处省略一堆代码
// 映射目标对象的属性到代理对象
const res = Reflect.get(
target,
key,
// if this is a proxy wrapping a ref, return methods using the raw ref
// as receiver so that we don't have to call `toRaw` on the ref in all
// its class methods
receiver,
)
if (!isReadonly) {
// 依赖收集的关键
track(target, TrackOpTypes.GET, key) // 重点, 典中典
}
// 返回映射后的结果
return res
}
}
当触发 track 时, 检查当前是否有活跃的 effect 若存在则需要将 target 响应式目标对象 与 effect 进行关联, 这里有一个 targetMap ,它就是用于保存 target 与 effect 映射关系, 以 target 为键, 这样在 trigger 时就可以快速找到触发的 effect ,
要注意的是,在 targetMap 中 effct 并不是以 value 的形式保存的,结合代码,具体怎样关联需要理解下文中 link 部分的代码, 下图的数据结构,或许可以让你了解里面存了哪些数据
位置: packages\reactivity\src\dep.ts
class Dep implements ReactiveNode {
// 订阅者链表头指针
// 指向第一个依赖该属性的 Link 节点
_subs: Link | undefined = undefined
// 订阅者链表尾指针
// 指向最后一个依赖该属性的 Link 节点
subsTail: Link | undefined = undefined
// 状态标志位
flags: ReactiveFlags = ReactiveFlags.None
/**
* 构造函数
* @param map - 所属的 KeyToDepMap(target → key → Dep 的映射)
* @param key - 该 Dep 对应的属性键
*/
constructor(
private map: KeyToDepMap,
private key: unknown,
) {}
/**
* 获取订阅者链表
* 实际上是 getter,返回 _subs
*/
get subs(): Link | undefined {
return this._subs
}
/**
* 设置订阅者链表
* 使用 setter,当没有订阅者时自动从 map 中删除
*/
set subs(value: Link | undefined) {
this._subs = value
// 当没有订阅者时,从 map 中删除该 key
// 这样可以节省内存
if (value === undefined) {
this.map.delete(this.key)
}
}
}
type KeyToDepMap = Map<any, Dep>
// 主依赖映射表
export const targetMap: WeakMap<object, KeyToDepMap> = new WeakMap()
/**
* 追踪响应式属性访问
* 当访问一个响应式属性时,调用此函数建立依赖关系
*
* 核心数据结构:
* targetMap: WeakMap<object, Map<key, Dep>>
* ↓
* {target: {key: Dep}}
*
* @param target - 响应式目标对象(如 reactive 创建的代理对象)
* @param type - 访问类型(GET / HAS 等)
* @param key - 访问的属性键(如 'name')
*/
export function track(target: object, type: TrackOpTypes, key: unknown): void {
// 1. 检查是否有活跃的 effect(正在执行)
// 如果没有,说明不是 effect 触发的访问,不需要追踪
if (activeSub !== undefined) {
// 2. 获取或创建 target 对应的 depsMap
// depsMap: Map<key, Dep> - 存储该 target 所有属性的依赖
let depsMap = targetMap.get(target)
if (!depsMap) {
// 如果不存在,为该 target 创建一个新的 Map
targetMap.set(target, (depsMap = new Map()))
}
// 3. 获取或创建 key 对应的 Dep
// Dep 存储了所有依赖该属性的 effect
let dep = depsMap.get(key)
if (!dep) {
// 如果不存在,创建一个新的 Dep
depsMap.set(key, (dep = new Dep(depsMap, key)))
}
// 4. 开发环境:调试追踪
if (__DEV__) {
onTrack(activeSub!, {
target,
type,
key,
})
}
// 5. 建立 Dep 和 Effect 之间的双向链接
// 这是最核心的一步:让 dep 知道有哪些 effect 依赖它
link(dep, activeSub!)
}
}
在 link 中将 dep 与 effect 做关联, 重点看情况4 创建新的 Link 节点的过程, 结合代码与下图的数据格式或许可以帮助你理解 dep 与 sub 双向链表的结构

位置: packages\reactivity\src\system.ts
/**
* 建立 dep(依赖)和 sub(订阅者)之间的双向链表连接
* 用于追踪响应式依赖关系:dep 知道哪些 sub 依赖它,sub 知道它依赖哪些 dep
*
* @param dep - 响应式依赖(如 reactive 对象的具体属性)
* @param sub - 订阅者(如 ReactiveEffect 或 Computed)
*/
export function link(dep: ReactiveNode, sub: ReactiveNode): void {
// 获取 sub 的依赖链表尾部
const prevDep = sub.depsTail
// 情况1: 如果 dep 已经在链表尾部,说明已经是最新的,无需处理
if (prevDep !== undefined && prevDep.dep === dep) {
return
}
// 获取可能的下一个 dep(从尾部或头部)
const nextDep = prevDep !== undefined ? prevDep.nextDep : sub.deps
// 情况2: 如果 dep 已经在链表中(不在尾部),更新版本号并移到尾部
if (nextDep !== undefined && nextDep.dep === dep) {
nextDep.version = globalVersion
sub.depsTail = nextDep
return
}
// 获取 dep 的订阅者链表尾部
const prevSub = dep.subsTail
// 情况3: 如果 sub 已经在 dep 的订阅者链表尾部,说明已连接
if (
prevSub !== undefined &&
prevSub.version === globalVersion &&
prevSub.sub === sub
) {
return
}
// 情况4: 创建新的 Link 节点,同时加入到两个链表
// 1. 将新节点设为 sub.depsTail 和 dep.subsTail
const newLink =
(sub.depsTail =
dep.subsTail =
{
// 版本号,用于判断连接是否最新
version: globalVersion,
dep,
sub,
// 保存前后的指针,用于双向链表
prevDep,
nextDep,
prevSub,
nextSub: undefined,
})
// 2. 更新 dep 链表的指针
if (nextDep !== undefined) {
nextDep.prevDep = newLink
}
if (prevDep !== undefined) {
prevDep.nextDep = newLink
} else {
// 如果链表为空,设置头部
sub.deps = newLink
}
// 3. 更新 sub 链表的指针
if (prevSub !== undefined) {
prevSub.nextSub = newLink
} else {
// 如果链表为空,设置头部
dep.subs = newLink
}
}
以上的过程已经执行完我们用法中的 effct(...) 代码, 我们知道了 当使用 effect 的时候会默认执行一次 用户fn 进行依赖收集,
完成了第一次的依赖收集, 下面我们要看一下,当我们对 reactive 代理的对象内进行改变时, 又是如何触发 effect 内的 用户fn 的
当我们修改 reactive 代理对象的属性时, 会触发 MutableReactiveHandler 内的 set 函数, 我们在讲 reactive 的时候有说过,在 set 方法中会执行 trigger 来触发对应的 effect
位置: packages\reactivity\src\baseHandlers.ts
class MutableReactiveHandler extends BaseReactiveHandler {
// 构造函数
constructor(isShallow = false) {
// 调用父类构造函数
// 初始化是否只只读对象为 false
super(false, isShallow)
}
// 处理代理的 set 操作
set(
target: Record<string | symbol, unknown>,
key: string | symbol,
value: unknown,
receiver: object,
): boolean {
let oldValue = target[key] // 获取旧值
// 此处省略了一堆代码
// key 是否存在于目标对象中
const hadKey = isArrayWithIntegerKey
? Number(key) < target.length
: hasOwn(target, key)
// 调用 Reflect.set 方法设置属性值
const result = Reflect.set(
target,
key,
value,
receiver,
)
// don't trigger if target is something up in the prototype chain of original
// 不触发原型链上的对象的更新
// 只有当 receiver 是 target 本身的代理时才触发
// (而不是原型链上某个对象的代理)
if (target === toRaw(receiver)) {
// 如果 key 之前不存在,说明是新增属性
if (!hadKey) {
// 触发 ADD 类型的事件(新增)
trigger(target, TriggerOpTypes.ADD, key, value)
}
// 如果 key 已存在且值发生变化
else if (hasChanged(value, oldValue)) {
// 触发 SET 类型的事件(更新)
trigger(target, TriggerOpTypes.SET, key, value, oldValue) // 重要!!!
}
}
return result
}
}
当响应式更新的时候 会触发trigger 方法,来通知所有依赖的effect重新执行, 最后
位置: packages\reactivity\src\dep.ts
/**
* 触发响应式更新
* 当响应式数据发生变化时,调用此函数通知所有依赖的 effect 重新执行
*
* @param target - 响应式目标对象
* @param type - 触发类型(SET / ADD / DELETE / CLEAR)
* @param key - 变化的属性键(可选)
* @param newValue - 新值
* @param oldValue - 旧值
* @param oldTarget - 旧的目标对象(用于 Map/Set 操作)
*/
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
): void {
// 1. 从 targetMap 中获取该 target 的依赖映射
const depsMap = targetMap.get(target)
if (!depsMap) {
// 如果从未追踪过,直接返回
// (比如直接修改了非响应式对象)
return
}
// 2. 定义执行函数 - 触发单个 Dep 的订阅者
const run = (dep: ReactiveNode | undefined) => {
if (dep !== undefined && dep.subs !== undefined) {
// 这里省略了一些代码 ...
// 传播更新给所有订阅者
propagate(dep.subs)
// 浅层传播(用于处理嵌套响应式)
shallowPropagate(dep.subs)
// 这里省略了一些代码 ...
}
}
// 3. 开始批量更新
startBatch()
// 4. 根据触发类型处理
if (type === TriggerOpTypes.CLEAR) {
// 清空整个集合(如 reactive Map/Set 被 clear)
// 触发所有依赖该 target 的 effect
depsMap.forEach(run)
} else {
// 省略了一堆代码 ...
// 5. 触发特定 key 的依赖
// 对于 SET | ADD | DELETE 操作
if (key !== void 0 || depsMap.has(void 0)) {
run(depsMap.get(key))
}
// 省略了一堆代码 ...
}
// 8. 结束批量更新
endBatch()
}
propagate 传播更新,将需要触发的 effect 添加到 notifyBuffer 中, 在刷新通知缓冲区 flush 时会通知 effect
位置: packages\reactivity\src\system.ts
// 通知缓冲区
const notifyBuffer: (Effect | undefined)[] = []
let notifyBufferLength = 0
/**
* 传播更新
* 遍历并触发所有依赖该属性的 effect(订阅者)
* 使用深度优先遍历 + 栈处理嵌套的订阅者(如 computed)
*
* @param link - 订阅者链表中的第一个 Link 节点
*/
export function propagate(link: Link): void {
// 下一个订阅者
let next = link.nextSub
// 栈,用于保存分支节点(处理嵌套订阅者)
let stack: Stack<Link | undefined> | undefined
// 深度优先遍历标签
top: do {
// 获取当前 Link 指向的订阅者(ReactiveEffect 或 Computed)
const sub = link.sub
let flags = sub.flags
// 只处理有效的订阅者:Mutable(可变的,如 computed)| Watching(正在追踪的,如 effect)
if (flags & (ReactiveFlags.Mutable | ReactiveFlags.Watching)) {
// 情况1: 没有任何标志位,直接设置为 Pending
if (
!(
flags &
(ReactiveFlags.RecursedCheck | // 递归检查
ReactiveFlags.Recursed | // 递归中
ReactiveFlags.Dirty | // 已脏
ReactiveFlags.Pending) // 待处理
)
) {
sub.flags = flags | ReactiveFlags.Pending
}
// 情况2: 没有 RecursedCheck 和 Recursed,重置标志
else if (
!(flags & (ReactiveFlags.RecursedCheck | ReactiveFlags.Recursed))
) {
flags = ReactiveFlags.None
}
// 情况3: 有 Recursed 但没有 RecursedCheck
else if (!(flags & ReactiveFlags.RecursedCheck)) {
sub.flags = (flags & ~ReactiveFlags.Recursed) | ReactiveFlags.Pending
}
// 情况4: 有 Dirty 或 Pending,且链接有效
// 需要标记为递归执行
else if (
!(flags & (ReactiveFlags.Dirty | ReactiveFlags.Pending)) &&
isValidLink(link, sub)
) {
// 标记为递归执行 + 待处理
sub.flags = flags | ReactiveFlags.Recursed | ReactiveFlags.Pending
// 只保留 Mutable 标志
flags &= ReactiveFlags.Mutable
}
// 情况5: 其他情况,跳过
else {
flags = ReactiveFlags.None
}
// 如果是 Watching(effect),加入待通知队列
if (flags & ReactiveFlags.Watching) {
notifyBuffer[notifyBufferLength++] = sub as Effect
}
// 如果是 Mutable(computed),需要继续传播给它的订阅者
if (flags & ReactiveFlags.Mutable) {
const subSubs = sub.subs
if (subSubs !== undefined) {
// 切换到子订阅者链表
link = subSubs
// 如果还有更多订阅者,保存当前分支到栈
if (subSubs.nextSub !== undefined) {
stack = { value: next, prev: stack }
next = link.nextSub
}
continue // 继续处理子订阅者
}
}
}
// 移动到下一个订阅者
if ((link = next!) !== undefined) {
next = link.nextSub
continue
}
// 栈不为空,从栈中恢复分支节点
while (stack !== undefined) {
link = stack.value!
stack = stack.prev
if (link !== undefined) {
next = link.nextSub
continue top // 跳回循环开始
}
}
// 栈为空,遍历结束
break
} while (true)
}
/**
* 结束批量更新
* 当 batchDepth 降为 0 且有待通知的 effect 时,触发 flush 执行所有 effect
*
* 工作原理:
* 1. 减少 batchDepth(批量深度计数器)
* 2. 如果 batchDepth 变为 0,说明所有批量操作已完成
* 3. 如果有待通知的 effect(notifyBufferLength > 0),执行 flush
*/
export function endBatch(): void {
// 1. 减少批量深度
// 2. 如果批量深度变为 0(所有批量操作完成)且有待通知的 effect
if (!--batchDepth && notifyBufferLength) {
// 执行所有待通知的 effect
flush()
}
}
/**
* 刷新通知缓冲区
* 执行所有待通知的 effect
*
* 从 notifyBuffer 中取出所有 effect 并执行其 notify() 方法
* 执行完毕后重置缓冲区
*/
export function flush(): void {
// 遍历通知缓冲区中的所有 effect
while (notifyIndex < notifyBufferLength) {
// 取出当前索引的 effect
const effect = notifyBuffer[notifyIndex]!
// 清除缓冲区中的该位置(释放内存)
notifyBuffer[notifyIndex++] = undefined
// 触发 effect 执行
effect.notify()
}
// 重置索引和长度,准备下次使用
notifyIndex = 0
notifyBufferLength = 0
}
触发 effect.notify 时会执行 ReactiveEffect.run() 方法, run 方法会重新执行 用户fn , 重新进行依赖收集
位置: packages\reactivity\src\effect.ts
export class ReactiveEffect<T = any>
implements ReactiveEffectOptions, ReactiveNode
{
/**
* 依赖链表头指针 - 指向该 effect 依赖的第一个 dep
*/
deps: Link | undefined = undefined
/**
* 依赖链表尾指针 - 指向该 effect 依赖的最后一个 dep
*/
depsTail: Link | undefined = undefined
/**
* 订阅者链表头指针 - 指向订阅该 effect 的第一个订阅者
*/
subs: Link | undefined = undefined
/**
* 订阅者链表尾指针 - 指向订阅该 effect 的最后一个订阅者
*/
subsTail: Link | undefined = undefined
/**
* 状态标志位
* 初始值: Watching | Dirty
* - Watching: 表示正在追踪依赖
* - Dirty: 表示数据已变化,需要重新执行
* - STOP: 表示已停止
* - PAUSED: 表示已暂停
* - Recursed: 表示正在递归执行
* - ALLOW_RECURSE: 允许递归
*/
// 默认标志位为 Watching | Dirty
flags: number = ReactiveFlags.Watching | ReactiveFlags.Dirty
/**
* 清理函数数组 - 存储 effect 停止时需要执行的清理回调
* @internal
*/
cleanups: (() => void)[] = []
/**
* 清理函数数量
* @internal
*/
cleanupsLength = 0
// dev only - 调试选项:追踪时的回调
onTrack?: (event: DebuggerEvent) => void
// dev only - 调试选项:触发时的回调
onTrigger?: (event: DebuggerEvent) => void
// 用户传入的响应式函数
// @ts-expect-error
fn(): T {}
// 省略一堆...
/**
* 通知 effect 执行
* 触发依赖变化后的回调
*/
notify(): void {
// 只有在未暂停且数据发生变化时才执行
if (!(this.flags & EffectFlags.PAUSED) && this.dirty) {
this.run()
}
}
/**
* 执行 effect 函数
* 1. 如果已停止,直接执行 fn(不追踪依赖)
* 2. 否则清理旧的依赖,追踪新的依赖,执行 fn
* @returns fn 的返回值
*/
run(): T {
// 如果 effect 已停止,以非追踪模式执行
if (!this.active) {
return this.fn()
}
// 清理旧的依赖追踪
cleanup(this)
// 开始追踪依赖,保存之前的订阅者
const prevSub = startTracking(this)
try {
// 执行用户函数,返回其返回值
return this.fn()
} finally {
// 结束追踪依赖
endTracking(this, prevSub)
const flags = this.flags
// 检查是否需要递归触发
// 如果同时设置了 Recursed 和 ALLOW_RECURSE 标志
if (
(flags & (ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)) ===
(ReactiveFlags.Recursed | EffectFlags.ALLOW_RECURSE)
) {
// 清除 Recursed 标志,然后触发更新
this.flags = flags & ~ReactiveFlags.Recursed
this.notify()
}
}
}
// 省略一堆
}
5. 运行流程(调试流程)
- 编写调试代码, 打断点

- 创建
ReactiveEffect实例
- 进入构造函数, 保存 `fn`

- 执行
Reactive.run方法
- 开始进行依赖追踪

- 更改全局版本号, 请空依赖链表尾部, 重置状态标志

- 设置当前活跃的订阅者

- 执行
用户fn
fn中读取了响应式代理的属性,这时触发了响应式的get方法
- 触发依赖收集

- 建立相应的数据结构,让effect与 响应式关联起来

- 建立关联

- 结束依赖追踪


- 设置
reactive代理对象的属性值,
- 触发
MutableReactiveHandler.set, 执行trigger触发effect执行
- 获取
depsMap
- 开始批量更新


- 触发执行函数传播事件


- 结束批量更新


- 通知所有的
effect开run
- 开
run
run时又会触发get重新进行依赖收集

6. 问题
6.1 effet 中观察者模式的实现
依赖收集的实现本质上是观察者模式在响应式系统中的具体应用
在这个模式中:
- 观察目标 :是响应式数据(如
reactive对象或ref的.value)。 - 观察者 :是依赖于这些数据的实体,例如组件的渲染函数、
computed计算属性或watch侦听器。
其工作流程遵循一个清晰的闭环,可以分为三个核心步骤:
- 数据劫持:让数据"可观测" 这是依赖收集的基础。Vue 通过拦截对数据的访问和修改操作,使其变得"可观测"。在 Vue 2 中,这通过
Object.defineProperty为每个属性定义getter和setter实现;而在 Vue 3 中,则采用了更强大的Proxy对象来代理整个对象,从而能够监听动态增删属性、数组索引变化等更广泛的操作。 - 依赖收集:记录"谁用了我" 当组件渲染或副作用函数执行时,一旦访问了某个响应式属性,就会触发其
getter(或Proxy的get拦截器)。此时,Vue 会将当前正在执行的副作用函数 (在 Vue 2 中称为Watcher,在 Vue 3 中称为ReactiveEffect)记录下来,并将其添加到该属性对应的"依赖列表"中。这个过程就是"收集依赖"。在 Vue 3 的实现中,这个依赖关系通过一个核心的数据结构targetMap(一个WeakMap)来存储,其映射关系为:原始对象 -> 属性键 -> 依赖该属性的副作用函数集合。 - 派发更新:数据变了,通知"订阅者" 当响应式数据的值被修改时,会触发其
setter(或Proxy的set拦截器)。系统会从之前建立的依赖关系(即targetMap)中,找到这个属性对应的所有副作用函数集合,然后逐一执行它们(或通过调度器进行异步批量执行),从而触发视图的重新渲染或计算属性的重新计算。这就是"触发更新"或"派发更新"。
6.2 观察者模式 和 订阅发布模式有什么区别
观察者模式与发布-订阅模式是软件设计中两种用于实现对象间通信的经典模式,它们都旨在实现解耦,但在耦合程度、实现机制和适用场景上存在显著差异。根据搜索结果,两者的核心区别在于**是否存在一个独立的"调度中心"或"事件通道"**来中介发布者与订阅者之间的通信。
一、 核心概念与架构差异
- 观察者模式:直接依赖的"一对多"通知 观察者模式定义了一种一对多的依赖关系 ,当一个对象(称为"主题"或"被观察者")的状态发生改变时,它会直接通知 所有依赖于它的对象(称为"观察者"),并自动更新它们。在这种模式中,主题和观察者是互相感知、直接关联 的。观察者需要将自己注册到主题的观察者列表中,主题则负责维护这个列表并在状态变化时遍历列表、调用每个观察者的更新方法。其架构是面向目标和观察者编程的。
- 发布-订阅模式:通过中介的"消息范式" 发布-订阅模式是一种消息范式 。在这种模式下,消息的发送者(发布者)不会将消息直接发送给特定的接收者(订阅者),而是发布到某个调度中心、事件通道或消息代理 。订阅者向调度中心订阅感兴趣的消息类型,当匹配的消息发布时,由调度中心负责将消息推送给相应的订阅者。因此,发布者和订阅者彼此不知对方的存在,实现了彻底的解耦 。其架构是面向调度中心编程的。
二、 耦合度与通信机制对比
这是两种模式最本质的区别,决定了它们的灵活性和适用性。
| 对比维度 | 观察者模式 | 发布-订阅模式 |
|---|---|---|
| 耦合关系 | 紧耦合。观察者必须知道主题的存在并主动注册,主题也必须知道所有观察者的接口以进行通知。 | 松耦合。发布者和订阅者完全解耦,双方只与调度中心交互,无需知道对方的具体实现甚至存在。 |
| 通信中介 | 无中介。主题直接持有观察者引用列表,并负责通知。 | 有中介。必须存在一个调度中心(事件通道)来管理订阅关系、过滤和分发消息。 |
| 通知粒度 | 广播式通知 。主题状态变化会通知所有已注册的观察者,观察者通常收到相同的更新信息。 | 筛选式通知。订阅者可以声明只对特定类型或内容的消息感兴趣,调度中心会进行过滤,只有符合条件的订阅者才会收到通知。 |