Vue3中effect函数原理详解:从副作用收集到依赖追踪
本文深入解析Vue3中effect函数的实现原理。通过剖析ReactiveEffect类、targetMap全局变量及activeEffect指针,详细阐述依赖收集track与触发trigger的工作流程。同时说明computed和watch如何基于effect构建,揭示Vue3响应式系统的核心逻辑。
前言
在Vue3中,effect函数构成了响应式系统的核心基础,它被称为副作用函数(Side Effect Function)。接下来我们将探讨副作用函数的具体含义。
在Vue框架中,当数据target发生变化时,可能会引发其他数据或视图的更新。这些由target变化所导致的后续数据变更及视图渲染更新,统称为target变化的副作用。诸如watch、computed等API都会产生副作用,而它们的底层实现均依赖于effect函数。
首先我们来解析effect函数的运行原理。在深入之前,需要认识几个关键的全局变量。
targetMap
targetMap是一个WeakMap实例,用于存储{target -> key -> dep}的映射关系。其中,targetMap的key是需要进行响应式处理的原始对象,而value是一个Map对象。该Map的key对应原始对象的属性名,value则是与该属性关联的副作用函数Set集合。这里的副作用函数即我们需要追踪的依赖,也就是订阅者。
activeEffect
activeEffect变量用于保存当前正在执行的副作用函数,其类型为一个对象,effect的类型定义如下:
let activeEffect: ReactiveEffect | undefined
// effect类型
class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
run(){}
stop() {}
}shouldTrack
shouldTrack变量用于标识是否开启依赖收集功能。只有当shouldTrack的值为true时,系统才会执行依赖收集,即将副作用函数添加到依赖集合中。该变量的初始值为true。当执行run()方法时,会将shouldTrack设置为true,从而开启依赖收集流程。
effect
接下来分析副作用函数effect的具体实现原理。当依赖的数据发生变化时,副作用函数会被触发。这必然涉及到依赖的收集与追踪,因此后续我们将详细讲解track、trigger与effect三者之间的关系。
interface ReactiveEffectRunner<T = any> {
(): T
// ReactiveEffect就是上面提到的activeEffect的类型
effect: ReactiveEffect
}
function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 如果传入的fn本身就是effect,那么就直接执行 effect的副作用
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 将fn包装成effect
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
if (options.scope) recordEffectScope(_effect, options.scope)
}
// 不是延迟执行,直接执行副作用函数
if (!options || !options.lazy) {
_effect.run()
}
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}fn是传递给effect函数的副作用逻辑,通过fn将其传递给ReactiveEffect进行包装,从而将其转换为标准的effect类型。标准的effect类型对应的是ReactiveEffect类,该类具备active、deps、deps、run()、stop()等属性和方法。其中,run方法内部负责执行传入的副作用函数fn。然而,effect函数返回的并非ReactiveEffect类型,而是ReactiveEffectRunner类型。接口ReactiveEffectRunner中包含effect属性,其类型为ReactiveEffect。因此,effect函数最终需要执行runner.effect = _effect,并返回runner。我们传入的副作用逻辑将在effect的run()方法中执行,需要重点关注run方法的实现。
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
// 全局activeEffect指向当前执行的自身
activeEffect = this
// 开启依赖收集
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
// 执行副作用函数
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}在执行run方法时,会将全局activeEffect指针指向自身,即当前正在执行的effect实例,随后开启依赖收集标识位,并执行副作用函数。
至此,我们似乎尚未发现effect与track和trigger之间的联系。track、trigger函数与effect函数位于源码的同一文件中,它们必然存在关联。接下来我们将查看track函数的逻辑实现。
track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && 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()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}在研究vue的双向数据绑定时,需要进行依赖收集与依赖追踪。track即为依赖收集的过程。当我们通过响应式API初始化数据,或在模板中渲染某些变量时,就需要进行依赖收集,也就是调用track。这里的依赖指的是我们的副作用函数,而依赖收集的过程本质上就是收集副作用函数的过程。至于副作用,即前文所述的effect。这样就将两者关联起来了。在之前研究effect时提到的全局变量中,targetMap存储了{target -> key -> dep}的关系。依赖收集的过程就是将副作用存储起来的过程。因此在track函数中,传入的参数target对应的是targetMap的key。首先检查全局变量targetMap中是否存在该target,若不存在则初始化map并赋值。同理,通过key查找最终的副作用Set,若不存在则初始化设置。至此,我们完善了{target -> key -> dep}的关系,接下来看看trackEffects的实现。
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack({
effect: activeEffect!,
...debuggerEventExtraInfo!
})
}
}
}trackEffects的逻辑实际上是将当前激活的activeEffect放入dep中,从而完成依赖收集过程。trigger过程不再赘述,其作用是根据{target -> key -> dep}的关系层层查找,最终找到dep并执行其中的副作用函数。说到这里,可能仍有读者疑惑:如果不使用watch、computed等带有副作用的API,track 、trigger与effect之间又有何关系?那么track收集的副作用究竟是什么呢?
在此需要说明,关于副作用,除了watch、computed等API中用户手动传入的fn被视为副作用外,页面渲染本身也是一种副作用。使用Vue的双向数据绑定特性意味着,如果定义了一个响应式变量,且该变量在模板中被使用,那么该变量在模板中的渲染就是响应式的,这个渲染过程即为副作用,需要被收集到dep中。如果该变量还作为watch中fn的内部内容出现,则该变量的副作用将增加一项,需要被额外收集。因此,dep是一个Set数据结构。当该变量发生变化时,页面需要重新渲染,watch也需要重新计算,这就是trigger的过程。
当然,上述情况针对的是响应式数据。如果仅定义一个静态数据,则无需进行依赖收集,因为它不具备响应式特性。至此应该可以明白,effect与响应式机制密切相关。
computed
讲完effect,不难推断出computed和watch本质上也是一种effect,只是传入的参数不同。我们简要查看一下computed的实现。
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
// computed也是实例化effect
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}从computed的构造器中可以看出,computed也是通过实例化effect实现的,只是传入的参数有所区别,我们暂且称之为computedEffect。可以看到computedEffect的computed属性为true。
watch
注意:虽然watch依赖于effect函数,但在源码目录结构中,它并不属于reactivity目录,而是位于runtime-core目录下。
const effect = new ReactiveEffect(getter, scheduler)可以看到,watch中同样使用ReactiveEffect创建副作用,只不过多传入了一个scheduler参数。
总结
到此,关于Vue3中effect函数原理的文章介绍完毕。更多相关Vue3中effect函数的内容,请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!
JS语法错误会阻止加载吗?区分SyntaxError与资源加载失败
2026-11-05 16:08:46
JavaScript常见SyntaxError语法错误排查与修复方法
2026-11-05 13:58:44
JS中slice、substring和substr截取字符串的区别
2026-11-05 13:43:47
JS 转 TS 手动迁移最佳实践:从配置 tsconfig 到消除 any 类型
2026-11-05 11:53:32
为什么JS数组push后得到的是数字而不是数组?
2026-11-05 09:59:18
