Vue3源码解析之 computed

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

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

前言

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

案例

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

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 }:

之后赋值给 getter,setter 我们可以理解为一个空函数,之后创建一个 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 value 和 set value 两个方法,这也是和 ref 相同,赋值时需带上 .value 属性的原因。get value 会进行依赖收集,但是依赖触发并没有在 set value 中,而是在我们之前 ReactiveEffect 传入的第二个参数中。

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

之后执行 effect 函数,进行赋值 document.querySelector('#app').innerHTML = computedObj.value,从而触发 computed 的 get 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 脏变量为 true(ComputedRefImpl 构造函数默认为 true),所以之后设置为 false,再执行 self.effect.run() 进行赋值。我们知道 effect.run() 实际执行的是 fn() 方法,即 computed 传入的匿名函数 () => { return '姓名:' + obj.name },effect 函数执行完毕,页面呈现如下:

两秒后触发 obj 的 setter 行为,即执行 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 赋值操作,再次触发 computed 的 get 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 变量来控制 effect 中 run 方法的执行和 triggerRefValue 的触发。
  3. 想要访问计算属性的值,必须通过 .value ,因为它内部和 ref 一样是通过 get value 来进行实现的。
  4. 每次 .value 时都会执行 get value 方法,从而触发 trackRefValue 进行依赖收集。
  5. 在依赖触发时,需要谨记,先触发 computed 的 effect ,再触发非 computed 的 effect ,为的是多次 .value 赋值时造成死循环。

Vue3 源码实现

vue-next-mini

Vue3 源码解析系列

  1. Vue3源码解析之 源码调试
  2. Vue3源码解析之 reactive
  3. Vue3源码解析之 ref
  4. Vue3源码解析之 computed
相关推荐
专业程序开发源11 分钟前
springboot社区养老系统44071-计算机课程设计、毕业设计
vue.js·spring boot·后端·python·django·php·课程设计
小呆呆66618 分钟前
副业搞起来,小说,漫画,漫剧的成本优化思路
前端·后端·面试
Dovis(誓平步青云)37 分钟前
浇水提醒刚弹出又消失,植物状态别只存一个百分比
开发语言·前端·javascript·pdf·ecmascript·电脑
码艺-Alimjan1 小时前
Vben Admin 新增维吾尔语 Vben-Modal的关键坑之一
前端·javascript·vue.js
可乐鸡翅yeah_2 小时前
hls.js 手动自定义 http 请求 loader,修改请求头实战
开发语言·前端·javascript·网络协议·http·ecmascript·m3u8在线
IT_陈寒2 小时前
Vite静态资源导入这个坑我帮你们踩过了
前端·人工智能·后端
广州华水科技2 小时前
大坝安全监测解决方案:单北斗GNSS形变监测系统应用与维护
前端
星光开发者2 小时前
基于决策树随机森林算法的小说推荐系统的设计与实现-计算机毕设【课程设计】78485
vue.js·算法·微信小程序·django·php·课程设计·express
yivifu3 小时前
中文古籍电子书注释集成
前端·javascript·python·beautifulsoup·epub
vx_Biye_Design3 小时前
springboot高校选课系统82776-计算机课程设计、毕业设计
java·vue.js·spring boot·后端·spring·课程设计·express