Vue3.0源码学习——Computed

「这是我参与2022首次更文挑战的第25天,活动详情查看:2022首次更文挑战」。

前言

Vue3 Computed文档

Computed 函数会根据传入的getter中依赖的响应式对象,返回一个新的响应式对象,当依赖的对象发生变化,新的对象也会跟着变

在之前的文章 Vue3.0源码学习——初始化流程分析(3.patch过程)Vue3.0源码学习——更新流程分析 中学习了Vue3在初始化和更新的过程。在渲染函数 baseCreateRenderer 中有个副作用安装函数 setupRenderEffect,其中一个对象 effect 是从 ReactiveEffect 这个类创建的实例,会将组件更新函数 componentUpdateFn 作为参数传入,最终会在依赖发生变化时进行对应更新操作

本文将结合 ReactiveEffect 类和 Computed 函数源码进行学习

源码分析

  • 找到 computed 的位置 \packages\reactivity\src\computed.ts,首先可以看到源码中对 computed 做了几次重载,因为在使用 computed 时第一个参数可以是 getter 函数,也可以是一个具有 get 和 set 函数的对象

图片.png

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

  // 如果是函数,传入getter
  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    ...
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 创建computed的ref
  const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)

  ...

  return cRef as any
}
复制代码
  • 来到 ComputedRefImpl 中,constructor 中定义的 this.effect 就是用响应式副作用函数 ReactiveEffect 创建的实例,当 computed 的依赖发生变化就触发 triggerRefValue 然后重新执行 getter
class ComputedRefImpl<T> {
  ...

  constructor(
    getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean
  ) {
    // 创建响应式副作用,ReactiveEffect第二个参数scheduler会触发第一个参数fn执行
    this.effect = new ReactiveEffect(getter, () => {
      if (!this._dirty) {
        this._dirty = true
        triggerRefValue(this)
      }
    })
    this.effect.active = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
复制代码
  • 第一次当组件渲染会触发 computed 的 get value(),返回 self.effect.run() 的执行结果,其实就是返回的 getter 函数的执行结果,因此在使用时可以通过 computed.value 获取值
  get value() {
    // 可能被其他代理包装过,因此先拿到原始数据
    const self = toRaw(this)
    // 首次执行,收集依赖
    trackRefValue(self)
    // 立刻执行一次副作用
    if (self._dirty) {
      self._dirty = false
      self._value = self.effect.run()!
    }
    return self._value
  }

  set value(newValue: T) {
    this._setter(newValue)
  }
复制代码

ReactiveEffect 类片段,第一个参数 fn 就是传入的 getter

图片.png

调试

准备工作

首先做一个简单的例子,定义一个响应式的数据 state.counter,一个定时器每秒state.counter都会 +1,一个 doublecomputed 函数的返回值,等于 state.counter * 2

<body>
  <div id="app">
    <h1>computed</h1>
    <p>{{ state.counter }}</p>
    <p>{{ double }}</p>
  </div>
  <script>
    const app = Vue.createApp({
      setup(props, { emit, slots, attrs }) {
        const state = Vue.reactive({
          counter: 1
        })

        setInterval(() => {
          state.counter++
        }, 1000)

        const double = Vue.computed(() => state.counter * 2)

        return {
          state,
          double
        }
      }
    })

    app.mount('#app')
  </script>
</body>
复制代码
  • computed 函数中打断点,刷新页面,第一次执行 computed 函数,然后找到返回值 cRef 并打上断点,然后单步进入

图片.png

  • 进入 ComputedRefImpl 类,在 get value() 中打断点,然后继续执行

图片.png

  • 此时看一下堆栈信息,依次执行了这些函数,然后触发了 computed get value()
    • componentUpdateFn 组件更新函数
    • renderComponentsRoot 根组件渲染函数
    • render 页面渲染函数

图片.png

  • 可以看到就是页面上使用的 double 这个 computed 数据

图片.png

  • 回到 get value() 中,会继续调用 trackRefValue,单步进入,此时还没有手机到依赖 dep,因此会创建一个, 然后就会执行到 trackEffects 去收集依赖

图片.png

  • 可以看到收集到了有2个依赖
    1. 组件中使用的 double
    2. getter 中依赖的对象 state.counter
  • 这形成了一个依赖响应的“链”,依赖发生了变化,其他都会响应的变化 state.counter -> double -> 组件

图片.png

  • new ReactiveEffect 传入的第二个参数 scheduler 中打上断点,看一下更新的过程,然后将代码放过去,看到页面已经渲染出来了

图片.png

  • 查看调用栈可以看到是之前例子中设置的 setInterval 激活了 computed 的更新操作

图片.png

  • 继续往下走进入 triggerRefValue,会执行 triggerEffects

图片.png

  • triggerEffects 会循环依赖去做更新的操作

图片.png

Computed执行流程

图片.png

猜你喜欢

转载自juejin.im/post/7067107397407342606