likes
comments
collection
share

Vue3源码解析之 computed

作者站长头像
站长
· 阅读数 27

前言

Vue3 中响应式系统除了 reactiveref 这两个函数外,我们还需了解下 computedwatch 这两个函数,它们也是响应式系统的关键所在。我们知道 computed 计算属性是存在依赖关系,当依赖的值发生变化时计算属性也随之变化,接下来我们先看下 computed 是如何实现的。

案例

首先引入 reactiveeffectcomputed 三个函数,之后声明 obj 响应式数据和 computedObj 计算属性,接着又执行 effect 函数,该函数传入了一个匿名函数进行 computedObj 的赋值,最后两秒后又修改 objname 值。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <script src="../../../dist/vue.global.js"></script>
  </head>
  <body>
    <div id="app"></div>
    <script>
      const { reactive, effect, computed } = Vue
      // 创建响应式数据
      const obj = reactive({
        name: 'jc'
      })
      // 计算属性 触发 obj.name 的 get 行为
      const computedObj = computed(() => {
        return '姓名:' + obj.name
      })
      // effect 函数中 触发 计算属性的 get 行为
      effect(() => {
        document.querySelector('#app').innerHTML = computedObj.value
      })
      // 修改响应式数据的 name 值 触发 set 行为
      setTimeout(() => {
        obj.name = 'cc'
      }, 2000)
    </script>
  </body>
</html>

computed 实现

computed 函数定义在 packages/reactivity/src/computed.ts 文件下:

export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false
) {
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>
  // getterOrOptions 即传入的函数
  // () => { return '姓名:' + obj.name }
  const onlyGetter = isFunction(getterOrOptions) // 判断 getterOrOptions 是否为函数
  if (onlyGetter) {
    getter = getterOrOptions // 赋值 传入的函数
    setter = __DEV__
      ? () => {
          console.warn('Write operation failed: computed value is readonly') // 理解为空函数
        }
      : NOOP
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }
  // 创建 ComputedRefImpl 实例
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  if (__DEV__ && debugOptions && !isSSR) {
    cRef.effect.onTrack = debugOptions.onTrack
    cRef.effect.onTrigger = debugOptions.onTrigger
  }
  // 返回 实例
  return cRef as any
}

这段逻辑也容易理解,computed 函数接收一个 getterOrOptions 参数,即我们传入的匿名函数 () => { return '姓名:' + obj.name }

Vue3源码解析之 computed

之后赋值给 gettersetter 我们可以理解为一个空函数,之后创建一个 ComputedRefImpl 实例,并将其返回,我们再看下 ComputedRefImpl 构造函数:

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined

  private _value!: T
  public readonly effect: ReactiveEffect<T>

  public readonly __v_isRef = true
  public readonly [ReactiveFlags.IS_READONLY]: boolean

  public _dirty = true // 脏变量 关键
  public _cacheable: boolean

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
      }
    })
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    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) // 依赖收集
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
}

该构造函数会创建一个 ReactiveEffect 实例,这块逻辑我们前面文章也已经讲过,这里就不再具体描述,我们先看下返回的实例 effect

Vue3源码解析之 computed

另外我们还需关心 ReactiveEffect 传入的第二个参数 scheduler,该构造函数在 packages/reactivity/src/effect.ts 文件下:

// 传入的参数
() => {
    if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
    }
}

// ReactiveEffect 构造函数
export class ReactiveEffect<T = any> {
  // 省略
  constructor(
    public fn: () => T,
    public scheduler: EffectScheduler | null = null,
    scope?: EffectScope
  ) {
    recordEffectScope(this, scope)
  }
  // 省略
}  

scheduler 我们可以理解为一个调度器,这也是 computed 核心所在,该逻辑会进行依赖触发,我们稍后再来讲解。ComputedRefImpl 还定义了一个 _dirty 脏变量,该变量用法也之后来讲解。另外还定义了 get valueset value 两个方法,这也是和 ref 相同,赋值时需带上 .value 属性的原因。get value 会进行依赖收集,但是依赖触发并没有在 set value 中,而是在我们之前 ReactiveEffect 传入的第二个参数中。

此时 computed 函数执行完毕返回 ComputedRefImpl 实例对象:

Vue3源码解析之 computed

之后执行 effect 函数,进行赋值 document.querySelector('#app').innerHTML = computedObj.value,从而触发 computedget value 方法:

get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self) // 依赖收集
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
}

trackRefValue(self) 进行依赖收集,该方法在前面文章也讲到过。由于此时 _dirty 脏变量为 trueComputedRefImpl 构造函数默认为 true),所以之后设置为 false,再执行 self.effect.run() 进行赋值。我们知道 effect.run() 实际执行的是 fn() 方法,即 computed 传入的匿名函数 () => { return '姓名:' + obj.name }effect 函数执行完毕,页面呈现如下:

Vue3源码解析之 computed

两秒后触发 objsetter 行为,即执行 createSetter 方法进行 trigger 依赖触发(第一次),然后根据 name 属性获取到对应的 effects,该逻辑都在 packages/reactivity/src/effect.ts 文件下:

Vue3源码解析之 computed

之后 triggerEffects 函数遍历 effects,执行 triggerEffect(effect, debuggerEventExtraInfo)

export function triggerEffects(
  dep: Dep | ReactiveEffect[],
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  // spread into array for stabilization
  const effects = isArray(dep) ? dep : [...dep]
  // 处理死循环 先执行 computed 属性的 effect 再执行不含有 computed 属性的
  for (const effect of effects) {
    // 存在 computed 属性
    if (effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
  for (const effect of effects) {
    if (!effect.computed) {
      triggerEffect(effect, debuggerEventExtraInfo)
    }
  }
}

function triggerEffect(
  effect: ReactiveEffect,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
  if (effect !== activeEffect || effect.allowRecurse) {
    if (__DEV__ && effect.onTrigger) {
      effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
    }
    if (effect.scheduler) {
      effect.scheduler()
    } else {
      effect.run()
    }
  }
}

这里我们需要关注下 if (effect.scheduler) 判断逻辑,由于此时执行的 effect 含有 computed 属性,且存在 scheduler,则会执行 effect.scheduler() 方法:

Vue3源码解析之 computed

这就是之前我们提到的 ComputedRefImpl 构造函数中,创建 ReactiveEffect 实例时传入的第二个参数:

export class ComputedRefImpl<T> {
  // 省略
  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
      }
    })
   // 省略
  }

  // 省略
}

// 第二个参数
() => {
    if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this) // dirty 为 false 时 触发依赖
    }
}

因为在 computed 函数的 get value 方法中 _dirty 设置了 false,所以直接走判断逻辑,执行 triggerRefValue(this) 依赖触发(第二次),所以 computed 的依赖触发是在该逻辑中执行的,这里是关键。

我们再看下此时获取到的 effects

Vue3源码解析之 computed

所以根据判断逻辑直接走 effect.run(),我们知道执行 run 等于执行 fn 方法,即执行 effect 传入的匿名函数,之后执行 document.querySelector('#app').innerHTML = computedObj.value 赋值操作,再次触发 computedget value 方法:

get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self) // 依赖收集
    if (self._dirty || !self._cacheable) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
}

接着执行 self._value = self.effect.run()!,又再次执行 computed 传入的匿名函数 () => { return '姓名:' + obj.name } 重新赋值:

Vue3源码解析之 computed

代码执行完成,此时页面呈现修改后的值:

Vue3源码解析之 computed

总结

  1. computed 计算属性实际是一个 ComputedRefImpl 构造函数的实例。
  2. ComputedRefImpl 构造函数中通过 dirty 变量来控制 effectrun 方法的执行和 triggerRefValue 的触发。
  3. 想要访问计算属性的值,必须通过 .value ,因为它内部和 ref 一样是通过 get value 来进行实现的。
  4. 每次 .value 时都会执行 get value 方法,从而触发 trackRefValue 进行依赖收集。
  5. 在依赖触发时,需要谨记,先触发 computedeffect ,再触发非 computedeffect ,为的是多次 .value 赋值时造成死循环。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列