Vue3 源码学习 - Computed

computed

源码位于 packages/reactivity/src

计算属性是如何实现的呢?

ts 复制代码
// in computed.ts
export function computed<T>(
  getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
  debugOptions?: DebuggerOptions,
  isSSR = false,
) {
  // computed这个API有两种传入参数的方法:
  // 1. 只传getter
  // 2. 把getter和setter塞进options对象中
  // 所以下面的处理就是把对应的getter和setter取出来
  let getter: ComputedGetter<T>
  let setter: ComputedSetter<T>

  const onlyGetter = isFunction(getterOrOptions)
  if (onlyGetter) {
    getter = getterOrOptions
    setter = __DEV__
      ? () => {
          warn('Write operation failed: computed value is readonly')
        }
      : NOOP // 如果没有传setter则不允许修改
  } else {
    getter = getterOrOptions.get
    setter = getterOrOptions.set
  }

  // 构造一个ComputedRefImpl实例
  // 参数分别对应 getter, setter, isReadonly, isSSR
  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
}

ComputedRefImpl

类 ComputedRefImpl 是如何实现的呢?

注意:在早期的版本中,并没有区分 trigger 和 scheduler。(比如霍春阳书中就是说用 scheduler 来改变 computed 为 dirty、并触发一次 trigger;而后面对 dirty 的判断移动进 computed 的 effect 中,也就是 effect.dirty,并且 triggerEffects 中内置了对 effect 的脏等级设置)

ts 复制代码
export class ComputedRefImpl<T> {
  public dep?: Dep = undefined // 收集的effect

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

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

  public _cacheable: boolean

  /**
   * Dev only
   */
  _warnRecursive?: boolean

  constructor(
    private getter: ComputedGetter<T>,
    private readonly _setter: ComputedSetter<T>,
    isReadonly: boolean,
    isSSR: boolean,
  ) {

    // 创建一个自己的effect实例
    // 第一个参数是fn,第二个参数是trigger,第三个参数是scheduler(这里没有传)
    this.effect = new ReactiveEffect(
      () => getter(this._value),  // fn
      () =>  // trigger
        triggerRefValue(
          this, // ref
          this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect // dirtyLevel
            ? DirtyLevels.MaybeDirty_ComputedSideEffect
            : DirtyLevels.MaybeDirty,
        ),
    )
    this.effect.computed = this // 给effect绑定computed
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }

  get value() {
    // ... 这里暂时省略
  }

  // set value 没什么好说的, 就是把新值传给setter执行
  set value(newValue: T) {
    this._setter(newValue)
  }

  // #region polyfill _dirty for backward compatibility third party code for Vue <= 3.3.x
  get _dirty() {
    return this.effect.dirty
  }

  set _dirty(v) {
    this.effect.dirty = v
  }
  // #endregion
}

对 value 的 get 劫持

早期版本(v3.4.0)的 get 其实很简单:

ts 复制代码
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    trackRefValue(self)
    if (!self._cacheable || self.effect.dirty) {
      if (hasChanged(self._value, (self._value = self.effect.run()!))) {
        triggerRefValue(self, DirtyLevels.ComputedValueDirty)
      }
    }
    return self._value
  }

现在详细看看 get 的逻辑:

1. 检查当前computed是否改变,如果改变则触发trigger

第一个 if,查看当前computed是否脏且改变,如果是则 trigger 依赖了当前 computed 的所有 deps。

ts 复制代码
export class ComputedRefImpl<T> {
  // ...
  get value() {
    // the computed ref may get wrapped by other proxies e.g. readonly() #3376
    const self = toRaw(this)
    if (
      // 当前的effect实际上会被其他的ref收集(读取某个ref的时候), 如果这个ref更改则会触发triggerEffects, 从而让effect变脏(可能是dirty,也可能是MayBeDirty,因此需要靠effect实例的dirty的getter方法,进行查询,见下一个代码块)
      (!self._cacheable || self.effect.dirty) &&
      hasChanged(self._value, (self._value = self.effect.run()!))
    ) {
      triggerRefValue(self, DirtyLevels.Dirty) // 由于依赖的值发生了改变,那么computed的值也就跟着发生了改变,因此要trigger一下computed本身的这个ref
    }
    // ...
  }

  // ...
}

读取 self.effect.dirty 实际触发了 getter

ts 复制代码
export class ReactiveEffect<T = any> {
  // ...
  public get dirty() {
    // 如果是可能脏,则需要triggerComputed来最终确认是否为脏。
    if (
      this._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect ||
      this._dirtyLevel === DirtyLevels.MaybeDirty
    ) {
      this._dirtyLevel = DirtyLevels.QueryingDirty // 正在查询是否为脏
      pauseTracking()
      for (let i = 0; i < this._depsLength; i++) {
        // 遍历当前effect被收集进的dep,对这些dep的computed进行trigger
        const dep = this.deps[i]
        if (dep.computed) {
          triggerComputed(dep.computed) // triggerComputed
          if (this._dirtyLevel >= DirtyLevels.Dirty) {
            // 只要trigger了某个computed后当前的effect._dirtyLevel变"脏"了,说明结果就是脏了,不需要再去trigger其他的effect
            break
          }
        }
      }
      // 如果结果没变,还是Querying,说明不脏
      if (this._dirtyLevel === DirtyLevels.QueryingDirty) {
        this._dirtyLevel = DirtyLevels.NotDirty
      }
      resetTracking()
    }

    // 返回_dirtyLevel是否为脏
    return this._dirtyLevel >= DirtyLevels.Dirty
  }

  // 设置值的时候,只要不是"不脏"(0),就是"脏"
  public set dirty(v) {
    this._dirtyLevel = v ? DirtyLevels.Dirty : DirtyLevels.NotDirty
  }
  // ...
}

triggerComputed 做了什么?其实很简单,就是单纯读取一个 computed 的 value,从而再次触发这个 computed 的 get value 劫持

ts 复制代码
function triggerComputed(computed: ComputedRefImpl<any>) {
  return computed.value
}

也就是说,如果是 computed 就会一直查依赖的 computed 是否 dirty,直到找到一个 dirty 的 ref。此时整个链路上的 computed 都会变成 dirty。

复习一下 triggerRefValue 到 triggerEffects 的逻辑:

ts 复制代码
export function triggerRefValue(
  ref: RefBase<any>,
  dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
  newVal?: any,
) {
  ref = toRaw(ref)
  const dep = ref.dep
  if (dep) {
    triggerEffects(
      dep,
      dirtyLevel,
      __DEV__
        ? {
            target: ref,
            type: TriggerOpTypes.SET,
            key: 'value',
            newValue: newVal,
          }
        : void 0,
    )
  }
}

export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  pauseScheduling()
  for (const effect of dep.keys()) {
    // dep.get(effect) is very expensive, we need to calculate it lazily and reuse the result
    let tracking: boolean | undefined  // 只有当前effect和dep中的effect同一轮(trackId相同)才进行tracking
    if (
      effect._dirtyLevel < dirtyLevel &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      effect._shouldSchedule ||= effect._dirtyLevel === DirtyLevels.NotDirty // 由不脏变为其他脏状态, 说明需要调度scheduler
      effect._dirtyLevel = dirtyLevel // 更新脏等级
    }
    if (
      effect._shouldSchedule &&
      (tracking ??= dep.get(effect) === effect._trackId)
    ) {
      if (__DEV__) {
        // eslint-disable-next-line no-restricted-syntax
        effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
      }
      // 需要track和调度, 触发trigger
      // 对于普通的ref, 实际上是NOOP; 对于computedRef, 实际上是
      // () =>
      //  triggerRefValue(
      //    this,
      //    this.effect._dirtyLevel === DirtyLevels.MaybeDirty_ComputedSideEffect
      //      ? DirtyLevels.MaybeDirty_ComputedSideEffect
      //      : DirtyLevels.MaybeDirty,
      //  ),
      effect.trigger()  // 所以这里实际上执行了triggerRefValue(对computedRef),而triggerRefValue会转而再执行triggerEffects(对computedRef的dep), 也就是让依赖了当前computedRef的effect进行trigger

      // 
      if (
        // 不在运行或者允许递归
        (!effect._runnings || effect.allowRecurse) &&
        // 脏值不为DirtyLevels.MaybeDirty_ComputedSideEffect
        effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect
      ) {
        // 说明需要调度, 则将scheduler加入栈中, 然后把shouldSchedule设置为false
        effect._shouldSchedule = false
        if (effect.scheduler) { // 对于computed,实际没有scheduler,只有trigger
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  resetScheduling()
}

为什么要有 effect._dirtyLevel !== DirtyLevels.MaybeDirty_ComputedSideEffect​​ 的判断?实际上,只有在 computed.value 的 get 劫持的第3步(见下文),会在 trigger 的时候将 dirtyLevel 设置为 MaybeDirty_ComputedSideEffect​​。这里是为了阻止 computed 的 trigger 再触发 effect/watch/render。见这个PR

2. 重新 track 当前的 computed

这一步和 RefImpl 的 get 是一致的

ts 复制代码
export class ComputedRefImpl<T> {
  // ...
  get value() {
    // ...
    trackRefValue(self) // 追踪computedRef的effect(也就更新成新一轮的trackId)
    // ...
  }
  // ...
}
3. 如果当前 computed 的 effect 还是较脏,则再触发一次 trigger
ts 复制代码
export class ComputedRefImpl<T> {
  // ...
  get value() {
    // ...
    if (self.effect._dirtyLevel >= DirtyLevels.MaybeDirty_ComputedSideEffect) {
      if (__DEV__ && (__TEST__ || this._warnRecursive)) {
        warn(COMPUTED_SIDE_EFFECT_WARN, `\n\ngetter: `, this.getter)
      }
      triggerRefValue(self, DirtyLevels.MaybeDirty_ComputedSideEffect)
    }
    // ...
  }
  // ...
}
4. 最后返回_.value

这一步和 RefImpl 的 get 是一致的

ts 复制代码
export class ComputedRefImpl<T> {
  // ...
  get value() {
    // ...
    return self._value
  }
  // ...
}

总结

ComputedImpl.value 的 getter:

  • 相比于 RefImpl.value 的 getter,在 track 之前和之后各新增了一个步骤:

    1. 检查当前computed是否改变,如果改变则触发trigger(dirtyLevel为 Dirty)
    2. 如果当前 computed 的 effect 还是较脏,则再触发一次 trigger(dirtyLevel为MaybeDirty_ComputedSideEffect,这样不会因为这个computed 触发其他的effect.run,防止递归触发)

ComputedImpl.value 的 setter:

  • 如果构造函数传入了 setter,则直接执行 setter
  • 如果没有传入,则不允许修改(只读)

简单实现一下

具体实现的源码见我的demo仓库:Github - vue-learning

ts 复制代码
// computed.ts
import { ReactiveEffect } from "./effect.js";
import { trackRefValue, triggerRefValue, type Dep } from "./ref.js";

export class ComputedRefImpl<T> {
  public dep?: Dep = undefined
  public effect: ReactiveEffect<T>
  private _value!: T;
  public readonly __v_isRef = true

  constructor(public getter: Function, public setter?: Function) {
    this.effect = new ReactiveEffect(() => {
      console.log("computed.effect 的 fn")
      return getter(this._value)
    }, () => triggerRefValue(this));
    this.effect.computed = this;
    this.effect.dirty = true;
  }

  get value() {
    trackRefValue(this);
    if (this.effect.dirty) {
      console.log(this, "computed dirty, run");
      this._value = this.effect.run()
      triggerRefValue(this);
    }
    console.log("获取computed.value");
    return this._value;
  }
}

export function computed(getter: Function) {
  const cRef = new ComputedRefImpl(getter);
  return cRef as any;
}

引入使用

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>
</head>
<body>
  <div id="app">
    <div>
      <span id="ref-value">
      </span>
      <span id="ref-value-2"></span>
    
      <button id="ref1">点击增加myRef1数值</button>
    
    </div>
    <div>
      <span id="ref-value-3"></span>
      <button id="ref2">点击增加myRef2数值</button>
    </div>

    <div>
      <span id="computed-value">
      </span>
      <button id="computed">alert myComputed1.value</button>
    </div>
    <div>
      <span id="computed-value-2"></span>
    </div>
  
  </div>

  <script type="module">
    import { ref } from "./ref.js";
    import { effect } from "./effect.js";
    import { computed } from "./computed.js"
    const myRef = ref(666);
    effect(() => {
      document.querySelector("#ref-value").innerText = "myRef.value = " + myRef.value;
      document.querySelector("#ref-value-2").innerText = "after some op:" + (myRef.value % 100 + 10000);
    })
    const myRef2 = ref(111);
    effect(() => {
      document.querySelector("#ref-value-3").innerText = "myRef2.value = " + myRef2.value;
    })

    const myComputed = computed(() => {
      console.log("传入computed中的getter");
      return myRef.value + myRef2.value
    });

    effect(() => {
      document.querySelector("#computed-value").innerText = "(myRef1.value + myRef2.vale) myComputed.value = " + myComputed.value;
    })
  
    const myComputed2 = computed(() => myComputed.value - myRef2.value);
    effect(() => {
      document.querySelector("#computed-value-2").innerText = "(myComputed.value - myRef2.value) myComputed2.value = " + myComputed2.value;
    })

    const ref1Btn = document.getElementById("ref1");
    ref1Btn.addEventListener("click", () => {
      myRef.value++;
      console.log("myComputed", myComputed);
    })

    const ref2Btn = document.getElementById("ref2");
    ref2Btn.addEventListener("click", () => {
      myRef2.value++;
    })

    const computed1Btn = document.getElementById("computed");
    computed1Btn.addEventListener("click", () => {
      alert(myComputed.value);
    })
  </script>
  
</body>
</html>

结果

相关推荐
栈老师不回家43 分钟前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds1 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果2 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长2 小时前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱2 小时前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai2 小时前
uniapp
前端·javascript·vue.js·uni-app
帅比九日3 小时前
【HarmonyOS Next】封装一个网络请求模块
前端·harmonyos