Vue3.4+版本之前,计算属性只有在被读取时,才会触发评估检测(evaluate)逻辑,如果此时drity === true,则触发getter重新计算。而drity会在计算属性依赖项变化时,被更新为true,逻辑自洽,但并不完美,因为这里存在一个误区:依赖项改变并不意味着计算属性结果的改变
计算属性的惰性求值(lazy)
来看个简单的例子:
假设我们有一个项目列表,以及一个增加计数器的按钮。一旦计数器达到 100,我们想以相反的顺序显示列表(尽管它并不实际,但能说明问题)
注意: 此例子运行在vue3.4 版本之前
html 代码解读复制代码<template>
  <button @click="increase">
    Click me
  button>
  <br>
  <h3>
    List
  h3>
  <ul>
    <li v-for="item in sortedList">
      {{ item }}
    li>
  ul>
template>
<script setup>
import { ref, reactive, computed, onUpdated } from 'vue'
const list = reactive([1,2,3,4,5])
const count = ref(0)
function increase() {
  count.value++
}
const isOver100 = computed(() => count.value > 100)
const sortedList = computed(() => {
  // 想象一下这里有非常昂贵的计算
  return isOver100.value ? [...list].reverse() : [...list]
})
onUpdated(() => {
  console.log('component re-rendered!')
})
script>
不妨想想,当我们点击按钮101次后,我们的组件将重新渲染多少次?
答案:101次!
我们来拆解一下具体的执行过程:
- 点击按钮,count增加。此时组件不会更新,因为我们没有直接在template中使用count
- 由于count发生了改变,isOver100属性被标记为 脏(drity为true)。这意味着isOver100在下次被读取时,必须重新计算结果(惰性求值)
- 由于惰性求值(lazy),我们并不知道isOver100重新评估的结果是否仍会为false
- sortedList依赖- isOver100的结果,所以它也必须被标记为 脏。同样由于- sortedList也是计算属性,所以仍然会在下次读取时重新计算结果
- 由于template依赖了sortedList,且sortedList被标记为脏,所以组件需要重新渲染。
- 重新渲染组件,触发sortedList的取值,这个过程运行了可能昂贵的sortedList计算,尽管大部分情况下并不需要
这里真正的罪魁祸首是isOver100,它是一种经常性的计算,通常返回和上一次相同的值,最重要的是它是一种廉价操作,没有真正从缓存中受益。它在一种昂贵的计算(确实从缓存中受益)中使用时,触发了不必要的更新,可能会严重降低代码性能。
但这并不意味着,我们将
isOver100作为计算属性,是一个错误的选择,很多时候我们确实需要计算属性自动基于其他状态派生出目标状态的能力,在这种场景(非昂贵计算)下,计算属性的惰性计算(lazy)能力似乎没有必要,反而可能会导致性能问题,那么你可能需要 computedEager
这里我们忽略了一个问题,计算属性的依赖方是如何得知该计算属性的依赖项发生了变化?或者说计算属性的依赖项改变时是如何通知依赖该计算属性的watcher/effect重新计算或渲染的?
Vue3.4 版本之前
在vue3.4版本之前,计算属性的依赖会同步被依赖该计算属性的watcher/effct收集,以上述例子为例:
 在
在Vue2版本中,Ref count同时被isOver100/sortedList/template render依赖,因此Ref Count改变时,会同时触发isOver100/sortedList/template render的更新逻辑。
 
Vue3版本之后(vue3.4之前),依赖关系略有不同,但整体的执行过程一致
详细的执行过程如下:
- 点击按钮,count 增加,依次触发isOver100/sortedList/template render的更新逻辑
- isOver100为计算属性,触发更新时,将- drity设置为- true,等待下次取值时计算
- 同理,sortedList也为计算属性,也将drity设置为true,等待下次取值时计算
- template render为渲染函数,接收到依赖更新的消息后,会立即重新运行- render函数更新视图,- render函数,依赖- sortedList,所以会触发- sortedList取值
- sortedList重新计算,由于- sortedList依赖- isOver100,所以会递归触发- isOver100的取值逻辑
- isOver100重新计算,获取最新的- count值计算最新结果并返回
- sortedList拿到- isOver100的最新结果,重新计算后返回
- template render拿到- sortedList的最新结果,重新渲染
可以看到,如果在第7步时,能够发现isOver100的值没有改变,那么7、8两步的计算就不需要进行,尤其是减少template render的执行,会显著提高Vue性能。
幸运的是,Vue团队在3.4版本响应式系统做出了重大的升级,其中就包括这一点的优化,我们来看一下
Vue3.4+ 版本的改进
以Vue 3.4.0版本为例,computed内部依赖的ReactiveEffect对象新增了_dirtyLevel这个内部属性,值类型如下:
ts 代码解读复制代码// core-3.4.0/packages/reactivity/src/constants.ts
export enum DirtyLevels {
  NotDirty = 0,
  ComputedValueMaybeDirty = 1,
  ComputedValueDirty = 2,
  Dirty = 3,
}
分别代表影响effct是否重新执行的四种脏值状态,其中ComputedValueMaybeDirty/ComputedValueDirty这两个状态主要用在计算属性的场景中
计算属性的取值(.value)这块使用了hasChanged方法来判断新值是否发生改变
ts 代码解读复制代码// core-3.4.0/packages/reactivity/src/computed.ts
export class ComputedRefImpl { 
  private _value!: T
  // ...
  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
  }
  // ...
}
// core-3.4.0/packages/shared/src/general.ts
export const hasChanged = (value: any, oldValue: any): boolean =>
  !Object.is(value, oldValue)
这里有两处关键的逻辑,一个是self.effect.dirty属性的判断,另一个是triggerRefValue方法的触发
仍然以之前的例子作为切入点,分析一下:
- count值发生改变,触发- isOver100的更新,此时- isOver100的- _dirtyLevel更新为- Dirty
ts 代码解读复制代码// core-3.4.0/packages/reactivity/src/reactiveEffect.ts
export function trigger(
  target: object,
  type: TriggerOpTypes,
  key?: unknown,
  newValue?: unknown,
  oldValue?: unknown,
  oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
  // ...
  // Ref count 的deps中包含了 isOver100 这个计算属性
  for (const dep of deps) {
    if (dep) {
      triggerEffects(
        dep,
        DirtyLevels.Dirty,
        __DEV__
          ? {
              target,
              type,
              key,
              newValue,
              oldValue,
              oldTarget,
            }
          : void 0,
      )
    }
  }
  // ...
}
// core-3.4.0/packages/reactivity/src/effect.ts
export function triggerEffects(
  dep: Dep,
  dirtyLevel: DirtyLevels,
  debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
  pauseScheduling()
  for (const effect of dep.keys()) {
    if (!effect.allowRecurse && effect._runnings) {
      continue
    }
    if (
      effect._dirtyLevel < dirtyLevel &&
      (!effect._runnings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
    ) {
      const lastDirtyLevel = effect._dirtyLevel
      // 此处将 isOver100 的 _dirtyLevel 设置为 Dirty
      effect._dirtyLevel = dirtyLevel
      if (
        lastDirtyLevel === DirtyLevels.NotDirty &&
        (!effect._queryings || dirtyLevel !== DirtyLevels.ComputedValueDirty)
      ) {
        if (__DEV__) {
          effect.onTrigger?.(extend({ effect }, debuggerEventExtraInfo))
        }
        // 触发 isOver100 的 trigger 方法
        effect.trigger()
        if (effect.scheduler) {
          queueEffectSchedulers.push(effect.scheduler)
        }
      }
    }
  }
  resetScheduling()
}
- isOver100的- trigger方法中,会将依赖- isOver100的- effct _dirtyLevel设置为- ComputedValueMaybeDirty,即- sortedList计算属性
ts 代码解读复制代码// core-3.4.0/packages/reactivity/src/computed.ts
export class ComputedRefImpl { 
  public dep?: Dep = undefined
  private _value!: T
  public readonly effect: ReactiveEffect, 
    private readonly _setter: ComputedSetter, 
    isReadonly: boolean,
    isSSR: boolean,
  ) {
    this.effect = new ReactiveEffect(
      () => getter(this._value),
      // computed effct 计算属性的 trigger 方法
      () => triggerRefValue(this, DirtyLevels.ComputedValueMaybeDirty),
    )
    this.effect.computed = this
    this.effect.active = this._cacheable = !isSSR
    this[ReactiveFlags.IS_READONLY] = isReadonly
  }
  
  // ...
}
// core-3.4.0/packages/reactivity/src/ref.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, // 此处依赖isOver100的 effct 设置为 ComputedValueMaybeDirty 
      __DEV__
        ? {
            target: ref,
            type: TriggerOpTypes.SET,
            key: 'value',
            newValue: newVal,
          }
        : void 0,
    )
  }
}
- 此时,sortedList被设置为ComputedValueMaybeDirty,同时也会将template/render设置为ComputedValueMaybeDirty
ts 代码解读复制代码// core-3.4.0/packages/runtime-core/src/renderer.ts 1572
// create reactive effect for rendering
const effect = (instance.effect = new ReactiveEffect(
  componentUpdateFn,
  NOOP,
  // render函数内部的ReactiveEffect采用了scheduler调度的方式
  () => queueJob(update),
  instance.scope, // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => {
  // 此处调用的dirty方法 
  if (effect.dirty) {
    effect.run()
  }
})
effect.dirty方法是一个getter函数,内部逻辑如下:
ts 代码解读复制代码export class ReactiveEffectany > {
  // ...
  public get dirty() {
    if (this._dirtyLevel === DirtyLevels.ComputedValueMaybeDirty) {
      this._dirtyLevel = DirtyLevels.NotDirty
      this._queryings++
      pauseTracking()
      for (const dep of this.deps) {
        if (dep.computed) {
          triggerComputed(dep.computed)
          if (this._dirtyLevel >= DirtyLevels.ComputedValueDirty) {
            break
          }
        }
      }
      resetTracking()
      this._queryings--
    }
    return this._dirtyLevel >= DirtyLevels.ComputedValueDirty
  }
  // ...
}
function triggerComputed(computed: ComputedRefImpl<any>) {
  return computed.value
}
- 由于render函数中的effct deps依赖了sortedList这个计算属性,所以会触发计算属性的取值逻辑(代码见上),在取值逻辑中会递归的触发sortedList drity -> isOver100 drity
- isOver100 drity逻辑触发时,由于- _dirtyLevel为- Dirty,且大于- ComputedValueDirty,返回- ture, 触发后续的- hasChanged逻辑
- 在这个场景中,大部分情况下,hasChanged会返回false,不会触发triggerRefValue(self, DirtyLevels.ComputedValueDirty)这段逻辑,那么后续sortedList drity的判断中,ComputedValueMaybeDirty小于ComputedValueDirty,返回false,后续取值函数直接返回原值,不重新计算,render函数同理,不会触发effect.run()
- 如果isOver100的hasChanged返回true,触发triggerRefValue,在triggerRefValue中会将sortedList的efftc _dirtyLevel设置为ComputedValueDirty,后续sortedList drity的判断会返回true,继而递归导致render函数的effect.drity这段逻辑返回true,导致render函数重新运行
总的来说,Vue3.4之后,在保留计算属性已有特性(lazy)的基础上,通过_dirtyLevel这一机制,大大优化了Vue组件状态更新时的开销
补充
vue3.4版本之后,计算属性的getter方法触发时,可以通过参数获得之前的旧值,这在计算属性计算一些引用类型时比较有用,可以避免一些不必要的计算,文档地址
html 代码解读复制代码<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
// 这个计算属性在 count 的值小于或等于 3 时,将返回 count 的值。
// 当 count 的值大于等于 4 时,将会返回满足我们条件的最后一个值
// 直到 count 的值再次小于或等于 3 为止。
const alwaysSmall = computed((previous) => {
  if (count.value <= 3) {
    return count.value
  }
  return previous
})
script>
注意此方式在部分vue 3.5可能不可用(3.5.0/3.5.1)
 
                                    
评论记录:
回复评论: