Vue3源码解析之 computed

本文为原创文章,未获授权禁止转载,侵权必究!

本篇是 Vue3 源码解析系列第 4 篇,关注专栏

前言

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

案例

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

html 复制代码
<!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 文件下:

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 }

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

ts 复制代码
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

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

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 实例对象:

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

ts 复制代码
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 函数执行完毕,页面呈现如下:

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

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

ts 复制代码
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() 方法:

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

ts 复制代码
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

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

ts 复制代码
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 } 重新赋值:

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

总结

  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 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
相关推荐
正小安20 分钟前
如何在微信小程序中实现分包加载和预下载
前端·微信小程序·小程序
_.Switch2 小时前
Python Web 应用中的 API 网关集成与优化
开发语言·前端·后端·python·架构·log4j
一路向前的月光2 小时前
Vue2中的监听和计算属性的区别
前端·javascript·vue.js
长路 ㅤ   2 小时前
vite学习教程06、vite.config.js配置
前端·vite配置·端口设置·本地开发
长路 ㅤ   2 小时前
vue-live2d看板娘集成方案设计使用教程
前端·javascript·vue.js·live2d
Fan_web2 小时前
jQuery——事件委托
开发语言·前端·javascript·css·jquery
安冬的码畜日常2 小时前
【CSS in Depth 2 精译_044】第七章 响应式设计概述
前端·css·css3·html5·响应式设计·响应式
莹雨潇潇3 小时前
Docker 快速入门(Ubuntu版)
java·前端·docker·容器
Jiaberrr3 小时前
Element UI教程:如何将Radio单选框的圆框改为方框
前端·javascript·vue.js·ui·elementui
Tiffany_Ho4 小时前
【TypeScript】知识点梳理(三)
前端·typescript