Vue3中effect函数原理详解:从副作用收集到依赖追踪

作者:小括号

时间:2026-10-31

浏览:206

本文深入解析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函数的内容,请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章