Vue3-响应式原理源码阅读记录【2】

92 阅读10分钟

vue3 与 vue2 主要差异之一无疑是响应式实现上的改变。本文主要阐述响应式原理的实现方式解析以及核心源码阅读的注释理解。

响应式原理源码路径 packages/reactivity/src/.*\.ts,本文主要涉及 ref.tscomputed.tsdeferredComputed.ts

前面已经看了 reactiveeffectdep 的源码,稍微理解了一下依赖收集、依赖更新,但副作用函数是啥,与响应式有什么关系还不太理解

现在,我们接着继续阅读:

ref

接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 .value

ref 类型定义:

interface Ref<T> { value: T }

function ref<T>(value: T): Ref<UnwrapRef<T>>

源码中,ref的构建实际是方法 createRef(rawValue: unknown, shallow: boolean) 的执行,而 createRef 方法返回一个 RefImpl<T>实例,这个实例就是最终代理的 ref实例。

ref 方法定义:

export function ref(value?: unknown) {
  return createRef(value, false)
}
/**
 * 创建ref实例
 * @param rawValue 参数
 * @param shallow 浅形式
 * @returns RefImpl
 */
function createRef(rawValue: unknown, shallow: boolean) {
  // 如果参数已经是ref实例,返回参数本身
  if (isRef(rawValue)) {
    return rawValue
  }
  // 否则,对参数构建ref实例
  return new RefImpl(rawValue, shallow)
}

我们来看看 RefImpl类定义:

/**
 * ref类
 */
class RefImpl<T> {
  private _value: T // 代理数据 - 经过 reactive 包装
  private _rawValue: T // 原始数据 - 自动解 reactive 代理数据

  public dep?: Dep = undefined // 对应依赖集合 Dep<Set<RectiveEffect>>
  public readonly __v_isRef = true // ref对象标识 - isRef() 判断依据

  constructor(value: T, public readonly __v_isShallow: boolean) {
    this._rawValue = __v_isShallow ? value : toRaw(value) // 原始数据
    this._value = __v_isShallow ? value : toReactive(value) // 代理数据 - toReactive 如果是object才用reactive包装,否则返回值本身
  }

  // getter 拦截器
  get value() {
    // 收集依赖
    trackRefValue(this) // ref 自己维护一个dep依赖集合,收集当前effect,建议依赖关系
    return this._value
  }

  // setter 拦截器
  set value(newVal) {
    const useDirectValue =
      this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
    // 获取原始数据
    newVal = useDirectValue ? newVal : toRaw(newVal)
    // 判断原始数据是否改变
    if (hasChanged(newVal, this._rawValue)) {
      // 更新值
      this._rawValue = newVal
      this._value = useDirectValue ? newVal : toReactive(newVal)
      triggerRefValue(this, newVal) // 通知依赖更新
    }
  }
}

ref 实例,维护了一个私有字段_value字段保存当前值 和 私有字段_rawValue字段保存原始值,提供字段 .value 的 setter/getter 拦截器,对_value 字段的取值赋值操作进行代理,从而实现对 .value 字段依赖收集/依赖更新。

这里有一个要注意的是,如果不是浅形式,_value 会使用reactive包装,如果传入的参数是object值,可以递归转成响应式。

trackRefValue/triggerRefValue 调用trackEffectstriggerEffects ,这里就不重复描述了。


shallowRef / triggerRef

ref() 的浅层作用形式,内部值_value将会原样存储和暴露。源码中与 ref() 方法类似,使用 createRef 工具方法进行创建,差别就只有第二参数 shallow=true

shallowRef定义:

export function shallowRef(value?: unknown) {
  return createRef(value, true)
}

shallow=true,ref实例 _value 字段不会经过 reactive包装,不会对object值进行递归构造响应,只有实例字段 .value 的取值赋值是响应式的。

由于浅形式下,内部值 _value 原样暴露,如果_value原始数据是object类型,直接使用 .value 修改对象字段值,不会触发依赖更新,这时候需要通过 triggerRef(shallowRef) 强制触发依赖于一个浅层ref的副作用。

trigger 方法定义:

export function triggerRef(ref: Ref) {
  triggerRefValue(ref, __DEV__ ? ref.value : void 0) // 通知依赖更新
}

customRef

customRef 用于创建一个自定义的 ref,显式声明对其依赖追踪和更新触发的控制方式。

先看一下 customRef 的类型定义:

export type CustomRefFactory<T> = (
  track: () => void,
  trigger: () => void
) => {
  get: () => T
  set: (value: T) => void
}

接收 track / trigger 两个参数,返回一个带 自定义set/get方法 的对象。

从源码中,调用 customRef() 方法,是返回一个 CustomRefImpl 实例。

CustomRefImpl类定义:

class CustomRefImpl<T> {
  public dep?: Dep = undefined // 对应一个Dep 依赖集合

  private readonly _get: ReturnType<CustomRefFactory<T>>['get'] // customRef自定义get
  private readonly _set: ReturnType<CustomRefFactory<T>>['set'] // customRef自定义set

  public readonly __v_isRef = true // ref标识

  constructor(factory: CustomRefFactory<T>) {
    // 构造函数,接收一个工厂函数参数
    // 将track/trigger函数传入工厂函数,返回自定义 set/get 方法
    const { get, set } = factory(
      () => trackRefValue(this),
      () => triggerRefValue(this)
    )
    // 保存自定义 set/get
    this._get = get
    this._set = set
  }

  get value() {
    // .value 字段get代理,执行自定义get方法
    return this._get()
  }

  set value(newVal) {
    // .value 字段set代理,执行自定义set方法
    this._set(newVal)
  }
}

computed

接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。

computed 类型定义:

// 只读
function computed<T>( 
    getter: () => T, // 查看下方的 "计算属性调试" 链接
    debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>

// 可写的
function computed<T>(
    options: {
        get: () => T
        set: (value: T) => void 
    },
    debuggerOptions?: DebuggerOptions
): Ref<T>

computed 方法定义:

/**
 * @param getterOrOptions 只带 getter的只读computed 或者 带 getter/setter 可读可写computed
 * @param debugOptions computed配置项
 * @param isSSR ssr标识
 * @returns 
 */
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  // computed值的getter/setter拦截器方法
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions) // 传入一个函数,默认作为 getter
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly')
        }
      : NOOP
  } else {
    // 否则,从对象参数中获取 getter/setter
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 构造一个computed实例
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  // 注入 track/trigger 钩子函数
  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }

  return cRef as any
}

可以看出方法最终返回的是一个 ComputedRefImpl 实例。

ComputedRefImpl 类定义:

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined // 对应的Dep 依赖集合

  private _value!: T // computed内部值
  public readonly effect: ReactiveEffect<T> // computed内部副作用函数

  public readonly __v_isRef = true // ref标识 - computed 返回一个响应 ref
  public readonly [ReactiveFlags.IS_READONLY]: boolean = false // 只读标识

  public _dirty = true // 是否需要重新计算标识
  public _cacheable: boolean // computed值是否可缓存标识

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    // 接收 getter/setter 方法,使用 getter 作为副作用函数,同时声明一个匿名函数,作为 effect.scheduler 字段值,作为调度方法。
    // 这里补充 scheduler 是在调用 triggerEffect 通知更新依赖是,判断如果有 scheduler ,执行scheduler函数,否则 才执行副作用函数。
    // scheduler 目的是为了缓存 computer 计算,避免频繁计算computed导致占用内存。
    this.effect = new ReactiveEffect(getter, () => {
      // 如果需要重新计算computed,则打上dirty标识为true,并触发computed依赖更新
      // 当依赖重新读取computed值时,computed会重新结算,然后还原dirty标识为false,缓存新的computed值,避免重复计算
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.computed = this // 副作用函数对象绑定computed字段,进行双向绑定 computedRml = effectRml
    this.effect.active = this._cacheable = !isSSR // ssr不支持computed缓存,effect.active = false,computed不会进行依赖收集
    this[ReactiveFlags.IS_READONLY] = isReadonly // 标识是否为只读compued = 只有getter方法时
  }

  get value() {
    // .value get代理
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this) // 获取原始数据
    trackRefValue(self) // 收集依赖
    // 如果不进行缓存,每次取值都要重新计算cpmputed
    if (self._dirty || !self._cacheable) {
      // 默认计算一次computed(执行getter),同时打上dirty=false,不需要重新计算值
      self._dirty = false
      self._value = self.effect.run()! // 执行一次副作用 = 执行 getter = 计算computed值
    }
    // 返回computed内部值
    return self._value
  }

  set value(newValue: T) {
    // .value set代理 - 执行自定义setter方法
    this._setter(newValue)
  }
}

这里有一个需要留意的,computed 的getter方法,是作为一个副作用函数,使用getter方法声明了一个 effect副作用对象,并与computed实例双向关联。

使用 dirty 标识来控制computed是否需要重新计算,当依赖更新,不会直接执行副作用函数,而是执行打上dirty标识,等到触发computed的getter时,才会重新计算computed值。


deferredComputed

目录里还有一个 deferredComputed 声明定义,类似computed,但是计算computed值的时机有差异,让我们来看看具体差异在哪?

再看 deferredComputed 之前,先看几个准备方法:

const tick = /*#__PURE__*/ Promise.resolve() // 执行清栈操作-放微任务队列后面
const queue: any[] = [] // 执行栈 - 计算computed方法
let queued = false // 是否有方法入栈 false-无,等待入栈;true - 有,执行清栈

// 调度方法入栈,等待清栈执行
const scheduler = (fn: any) => {
  queue.push(fn)
  if (!queued) {
    // 标识入栈,执行清栈
    queued = true // 防止重复执行清栈
    tick.then(flush)
  }
}

// 清栈方法
const flush = () => {
  for (let i = 0; i < queue.length; i++) {
    queue[i]()
  }
  queue.length = 0
  queued = false // 清栈完重置状态
}

这里主要维护了一个执行栈,用于computed计算方法的延迟执行。

computeddeferredComputed 方法实际返回的是一个 DeferredComputedRefImpl 类实例,与 ComputedRef 具有相同属性字段:

DeferredComputedRefImpl 类定义:

class DeferredComputedRefImpl<T> {
  public dep?: Dep = undefined // 对应dep 依赖集合

  private _value!: T // computed 内部值
  private _dirty = true // 重新计算computed标识
  public readonly effect: ReactiveEffect<T> // 对应的副作用函数对象

  public readonly __v_isRef = true // ref类型标识
  public readonly [ReactiveFlags.IS_READONLY] = true // 只读computed标识,默认为true

  constructor(getter: ComputedGetter<T>) {
    let compareTarget: any // 缓存的computed对比值
    let hasCompareTarget = false // 是否存在缓存对比值标识
    let scheduled = false // 执行schedule标识
    // 声明computed对应副作用函数对象,同时定义schedule参数(与computed的差异),带有一个 是否是computed触发副作用的标识(computedTrigger)
    this.effect = new ReactiveEffect(getter, (computedTrigger?: boolean) => {
      if (this.dep) {
        if (computedTrigger) {
          // 由依赖的computed触发
          compareTarget = this._value // 缓存当前computed值
          hasCompareTarget = true // 打上标识 true
        } else if (!scheduled) {
          // 获取computed对比值
          const valueToCompare = hasCompareTarget ? compareTarget : this._value
          scheduled = true // 执行schedule标识 ture
          hasCompareTarget = false // 重置是否存在缓存对比值 false
          // 对比方法入栈,等待出栈
          // 此时如果有computed更新,scheduled = true 的限制不会重复入栈
          scheduler(() => {
            // 调用私有方法_get,如果响应有变化,会触发重新计算computed
            if (this.effect.active && this._get() !== valueToCompare) {
              // computed有更新(对比差异),需要触发依赖更新
              triggerRefValue(this)
            }
            scheduled = false // 重置执行schedule标识 false
          })
        }
        // chained upstream computeds are notified synchronously to ensure
        // value invalidation in case of sync access; normal effects are
        // deferred to be triggered in scheduler.
        for (const e of this.dep) {
          if (e.computed instanceof DeferredComputedRefImpl) {
            // 响应更新,执行computed副作用函数 effect.scheduler(),缺少参数,
            // 这里需要重新调用触发此computed更新
            e.scheduler!(true /* computedTrigger */)
          }
        }
      }
      this._dirty = true
    })
    this.effect.computed = this as any
  }

  // 私有get方法,用于对比computed值,用于重新计算computed
  private _get() {
    // 判断是否需要重新计算computed值
    if (this._dirty) {
      this._dirty = false // 重新计算,重置状态为false
      return (this._value = this.effect.run()!) // 重新计算computed值
    }
    return this._value
  }

  // 公共 .value getter方法,用于收集依赖
  get value() {
    trackRefValue(this) // 同computed 收集依赖
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    // computed 可能会被其他reactive包装,需要获取原始数据
    // this 指向 - reactive 通过Proxy代理,Reflect.get 取值
    return toRaw(this)._get() // 返回computed内部值
  }
}

对比 computed,当computed依赖的响应字段更新时,调用computed的内部副作用函数,没有直接计算而是打上重新计算标识dirty(computed缓存),然后通知依赖computed的其他reactive更新取读取computed值,这时首个读取computed值的依赖触发computed getter钩子,会对computed重新计算后返回新值。

DeferredComputedRefImpl 当依赖的响应字段更新后,打上 deferredComputed 需要重新计算的标识dirty,对当前computed值进行缓存,并将一个schedule加入执行栈,放到微任务队列后,等待执行时,会调用内部方法_get 对computed值重新计算,如果新值不等于旧值,才通知依赖此computed的其他reactive更新去获取新值。

错误理解:e.scheduler!(true /* computedTrigger */) 一直在循环执行。

差点绕不出来,重新捋一下:

当响应字段更新,触发deferredComputed的副作用函数,此时参数computedTrigger=false,将对比computed值逻辑(重新计算)加入执行栈queue,等待微任务队列flush;同时,会通知依赖此 deferredComputed其他deferredComputed进行更新,直接调用effect.schedule传入computedTrigger=true参数,使其缓存自身的computed值并标识hasCompareTaget,以及打上重新计算标识dirty

等到执行栈出栈后,如果deferredComputed值发生改变,会通知所有依赖此 deferredComputed 的reactive进行更新,其中 deferredComputed 会取缓存的值进行对比更新。


到这里,ref 和 computed 也阅读完了,现在开始找到熟悉的感觉了。如果有掌握vue2的响应式源码,到这里应该都能与之对应上了。

先小结一下,ref通过类字段的 setter/getter 拦截器实现响应式,类似vue2,并且利用reactive对object值的封装,实现递归响应构建,内置维护自己的dep 依赖集合。computed继承ref,实际上返回的是一个ref实例,同ref一样,也是通过 getter/setter 拦截器实现响应式,并且一个computed对应一个effect副作用实例,同时也维护自己的一个dep 依赖集合。

还剩最后两个内容,继续。。。