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
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean = false
public _cacheable: boolean
constructor(
getter: ComputedGetter,
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)
评论记录:
回复评论: