vue3 源码阅读 - computed ReactiveEffect

1,923 阅读4分钟

前言

ReactiveEffect 作为 vue3 响应式对象中的订阅者,他可以订阅响应式对象的变化并做出对应的变化

ReactiveEffect 对象会在这几种场景下创建:

  1. computed(接受一个 getter 函数,返回一个只读的响应式 ref 对象,即 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。)
  2. watch (侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数)
  3. watchEffect (立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。)
  4. render (页面渲染)

注意:本文只分析 computed ReactiveEffect 的实现原理,由于 computed ReactiveEffect 比较长后文我会用 ReactiveEffect 来替代 computed ReactiveEffect

ReactiveEffect

class ReactiveEffect<T = any> {
  // 是否活跃
  active = true
  // dep 数组,在响应式对象收集依赖时也会将对应的依赖项添加到这个数组中
  deps: Dep[] = []
  // 上一个 ReactiveEffect 的实例
  parent: ReactiveEffect | undefined = undefined

  // 创建后可能会附加的属性,如果是 computed 则指向 ComputedRefImpl
  computed?: ComputedRefImpl<T>
  
  // 是否允许递归,会被外部更改,
  allowRecurse?: boolean
  
  // 延迟停止
  private deferStop?: boolean
  
  // 停止事件
  onStop?: () => void
  
  // dev only
  onTrack?: (event: DebuggerEvent) => void
  // dev only
  onTrigger?: (event: DebuggerEvent) => void

  constructor(
    // 参数赋值给fn
    public fn: () => T,
    // 参数赋值给 scheduler
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    // 标记作用域
    recordEffectScope(this, scope)
  }
  
  run() {
  
    if (!this.active) {
      return this.fn()
    }
    
    // 存储最上层 ReactiveEffect 对象
    let parent: ReactiveEffect | undefined = activeEffect
    
    // 缓存 是否可以跟踪依赖 上一次的结果
    let lastShouldTrack = shouldTrack
    
    while (parent) {
      if (parent === this) {
        return
      }
      parent = parent.parent
    }
    
    
    try {
      // 父结点指向上一个 ReactiveEffect
      this.parent = activeEffect
      // 当前活跃的 ReactiveEffect
      activeEffect = this
      // 允许追踪依赖
      shouldTrack = true
      // 定义当前的 ReactiveEffect 层级
      trackOpBit = 1 << ++effectTrackDepth
      
      // 当前层级没超过最大层级限制
      if (effectTrackDepth <= maxMarkerBits) {
        // 初始化 ReactiveEffect 对应的 Dep 集合 标记
        initDepMarkers(this)
      } else {
        // 清除副作用,一般不会触发
        cleanupEffect(this)
      }
      // 执行构造函数传入的方法
      return this.fn()
    } finally {
      // 当前层级没超过最大层级限制,清空标记
      if (effectTrackDepth <= maxMarkerBits) {
        finalizeDepMarkers(this)
      }
      
      // 回到上一层 ReactiveEffect
      trackOpBit = 1 << --effectTrackDepth 
      activeEffect = this.parent
      shouldTrack = lastShouldTrack
      this.parent = undefined
      
      // 延迟停止
      if (this.deferStop) {
        this.stop()
      }
    }
  }
  stop() {
    // stopped while running itself - defer the cleanup
    if (activeEffect === this) {
      this.deferStop = true
    } else if (this.active) {
      cleanupEffect(this)
      if (this.onStop) {
        this.onStop()
      }
      this.active = false
    }
  }
}

上面代码注释对 ReactiveEffect 的变量做了一些说明,我们再介绍下其中比较关键的变量:

  • trackOpBit: 一个二进制变量,表示当前 ReactivEffect 的层级。
    • 在 ReactiveEffect 嵌套执行时产生,比如在 computed 中执行 computed,就会产生嵌套层级。
  • effectTrackDepth: 表示 trackOpBit 右移的位数。
    • 每产生一个嵌套层级就 +1 等到对应的嵌套层级执行完后就会回到上个层级的标记数。
  • activeEffect 当前活跃的副作用对象。
    • 会在 ref、reactive、computed 收集依赖时作为依赖项添加到对应的dep集合中。

computed

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  
  if (onlyGetter) {
    getter = getterOrOptions
    setter = NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  return cRef as any
}

export class ComputedRefImpl<T> {
  // dep集合,用来存储依赖向 ReactiveEffect
  public dep?: Dep = undefined
  
  // computed 值的引用
  private _value!: T
  
  // computed 内部创建的 ReactiveEffect 对象。 
  public readonly effect: ReactiveEffect<T>
  
  // 标记
  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean
      public _dirty = true
  // 默认为 true 不讨论服务端渲染场景
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
  
    // 创建 ReactiveEffect 实例
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    
    // 设置内部创建的 ReactiveEffect 对象 computed 属性
    this.effect.computed = this
    
    // 我们不讨论服务端渲染,这里为true
    this.effect.active = this._cacheable = !isSSR
    
    // 不显示传递 setter 就是只读
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    // 获取原始对象
    const self = toRaw(this)
    
    // 收集依赖
    trackRefValue(self)
    
    // 首次触发get,会进入这个判断,并调用 ReactiveEffect 对象的 run 方法。
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    // 返回对应的值
    return self._value
  }
    
  set value(newValue: T) {
    this._setter(newValue)
  }
}

从上面代码可以看出,computed 核心在于 class ComputedRefImpl,下面开始分析这个 class

ComputedRefImpl 内部结构:

  • 属性

    • publuc dep: 用来收集引用了这个computed 的ReactiveEffect 集合。
    • private _value: computed 实际的值。
    • public readonly effect: computed 创建的 ReactiveEffect 对象
    • public readonly __v_isReadonly: 是否只读。
    • public _dirty 脏检查标志,在 computed 依赖的响应式数据发生变化更新这个标记、
    • public _cacheable 是否缓存结果。
  • 构造函数:

    1. 创建 ReactiveEffect 对象并赋值到 effect 属性上。
    2. ReactiveEffect
  • get value

    1. 调用 trackRefValue 收集正在引用 computed.value 的 ReactiveEffect。
    2. 调用 self.effect.run() 更新全局标记,然后调用 getter(computed 的回调函数) 触发内部响应式变量的getter,将 ReactiveEffect 添加到对应依赖集合中。
    3. 返回 _value 得到结果

业务代码分析

<template>
    <h1>{{ b }}</h1>
    <button @click="onClick">按钮</button>
</template>

<script setup lang="ts">
import { computed, ref } from "vue";

const a = ref(1);
const b = computed(() => a.value);

const onClick = () => {
    a.value++;
};
</script>

简单介绍一下上面代码的关键逻辑:

  • 变量 a 是一个 ref 响应式对象。
  • 变量 b 是一个 computed 对象,它也具有响应式对象的特性。
  • 当 a.value 变化时,b 会响应 a.value 的变化计算出新的结果。

我们从初始化,点击按钮,重新渲染这三个阶段来分析 ComputedRefImpl 的内部变化:

初始化:

  1. 调用 computed 实例化 ComputedRefImpl,然后创建对应 ReactiveEffect 对象。
  2. template 中引用了 b ,触发 get value ,触发 ComputedRefImpl 的依赖收集逻辑。
  3. 执行 effect.run(), 然后调用传入的回调函数,春如的回调函数中有引用 a.value,触发 a 的依赖收集,将 ReactiveEffect 添加到 a 的 dep 集合中。

点击按钮:

  1. a.value 自增,接着通知 a 中的 dep 集合所有依赖响应它的变化,这个集合中包含 b 创建 ReactiveEffect。 2.调用 ReactiveEffect scheduler,这个方法里面包含了 b 通知依赖更新的逻辑,所以这时也会通知 b 的依赖者响应变化。

重新渲染:

  1. template 中引用了 b,触发 get value 随后将 render ReactiveEffect 添加到 b 的 dep 集合中,然后执行 effect.run()更新 内部 _value 的值。

依赖收集有去重的逻辑,因为去重逻辑不影响我们分析,所以我在上面忽略了这一过程

总结

ReactiveEffect 与响应式对象的交同上面三个阶段的描述所示,这个过程逻辑也是非常清晰的,关于 computed ReactiveEffect 分析就到这儿了,如果我在文中的表述有不恰当或者不准确的地方,请各位掘友不吝赐教。