Appearance
computed:计算属性的懒执行与缓存原理
在 Vue 的响应式系统中,reactive 和 ref 提供了可变的状态。computed(计算属性)则用于基于这些状态派生出新值。
computed 不只是一个简单的函数。它是一个具有**懒执行(lazy execution)和缓存(caching)**特性的响应式 ref。本节将深入分析其实现原理。
computed 的核心目标
假设我们需要一个 doubled 值,它总是 count.value * 2。
一种实现方式是使用 effect:
javascript
const count = ref(1)
const doubled = ref(0)
// 立即执行,并在 count 变化时“急切地”重新执行
effect(() => {
doubled.value = count.value * 2
})这种方式的缺陷在于没有缓存。如果 effect 被其他依赖触发,doubled 也会被不必要地重新计算,即使 count 并未改变。
computed 解决了这个问题:
javascript
const count = ref(1)
// 1. 【懒执行】
// 这行代码执行时,getter 函数 () => count.value * 2 并“不会”运行
const doubled = computed(() => count.value * 2)
// 2. 【缓存】
// 只有当 count 变化后,第一次访问 doubled.value 时,
// getter 函数才会重新运行computed 的核心目标就是实现这种高效的、按需计算的派生状态。
ComputedRefImpl:计算属性的实现
computed 的实现类是 ComputedRefImpl。理解它的关键在于它扮演两个角色:
- 作为“订阅者”:它内部的
effect会“订阅”它所依赖的数据(如count)。 - 作为“发布者”:它本身是一个
ref,拥有自己的dep。当其他effect(如render)读取它时,它需要被track(依赖收集)。
typescript
// core/packages/reactivity/src/computed.ts
export class ComputedRefImpl<T> {
// 作为“发布者”,它有自己的依赖列表 (dep)
public readonly dep: Dep = new Dep()
// 缓存的计算结果
private _value!: T
// 【关键】“脏”标记,用于实现缓存
private _dirty = true
// 【关键】它内部封装了一个 ReactiveEffect
// 这个 effect 的“fn”就是我们传入的 getter 函数
// 这个 effect 的“scheduler”是用来标记“脏”的
public readonly effect: ReactiveEffect
// 标识
public readonly [ReactiveFlags.IS_REF] = true
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T> | undefined
) {
this[ReactiveFlags.IS_READONLY] = !_setter
// 关键:创建一个 effect,但不立即 run()
this.effect = new ReactiveEffect(
// 1. 要执行的函数 (getter)
() => getter(),
// 2. 调度器 (scheduler)
() => {
// 这是 computed 的核心:
// 当依赖变化时,不要“立即重新计算”
// 而是“标记为脏”,并“通知自己的订阅者”
if (!this._dirty) {
this._dirty = true
// 触发“订阅”了此 computed 的 effect (比如 render)
this.dep.trigger()
}
}
)
}
}get value:懒执行与缓存的实现
ComputedRefImpl 在构造时(constructor)并不会执行 getter 函数。真正的计算发生在 .value 属性的 get 访问器中。
typescript
// ComputedRefImpl 的 get value()
get value(): T {
// 1. 【发布】
// 调用 this.dep.track(),
// 让正在运行的 activeEffect (比如 render) 来订阅自己
this.dep.track()
// 2. 【检查缓存】
// 检查“脏”标记,如果为 true,说明缓存失效,需要重新计算
if (this._dirty) {
this._dirty = false // 标记为“干净”,防止下次重复计算
// 3. 【执行】
// 运行内部的 effect (this.effect.run())
// run() 会设置 activeEffect,然后执行 getter
// getter 在执行时,会“订阅”它所依赖的数据(如 count)
this._value = this.effect.run()
}
// 4. 【返回】
// 如果 _dirty 为 false,直接返回上次缓存的 _value
return this._value
}get value 的逻辑实现了懒执行和缓存:
- 只有在
_dirty为true时,才执行this.effect.run()(即getter函数)。 - 否则,无论访问多少次,都只返回已缓存的
_value。
缓存失效 (scheduler):依赖如何通知 computed?
_dirty 标记(缓存)是如何失效的?这依赖于 constructor 中定义的 scheduler(调度器)。
一个完整的更新流程如下:
创建:
const count = ref(1)const doubled = computed(() => count.value * 2)doubled被创建。_dirty = true。doubled.effect被创建,但未运行。
首次访问(
render执行):render访问doubled.value,触发doubled.get value()。doubled.dep.track():render开始订阅doubled。if (this._dirty):检查到_dirty为true。this._dirty = false:标记为“干净”(缓存有效)。this.effect.run():执行getter()。- 在
run()内部,count.value被访问,触发count.dep.track()。 doubled.effect订阅了count。_value被缓存为2。get value()返回2。
依赖变化:
count.value = 3执行。count的set value()被触发。count.dep.trigger()被调用。trigger找到了count的订阅者,即doubled.effect。trigger调用doubled.effect.scheduler()(而不是run())。
scheduler执行:computed的scheduler被执行。this._dirty = true:“脏”标记被设置(缓存失效)!this.dep.trigger():doubled通知它自己的订阅者(即render):“我的值可能变了,你需要重新运行!”
二次访问(
render重新执行):render重新运行,再次访问doubled.value,触发doubled.get value()。doubled.dep.track():render再次订阅doubled。if (this._dirty):检查到_dirty为true。this._dirty = false:标记为“干净”。this.effect.run():重新执行getter。- ... (重新订阅
count) ... _value被缓存为6。get value()返回6。
可写计算属性 (set value)
如果 computed 接收的是一个带 get 和 set 的对象,ComputedRefImpl 也会实现 set value 访问器。
typescript
// ComputedRefImpl 的 set value()
set value(newValue: T) {
// 调用用户提供的 setter
if (this._setter) {
this._setter(newValue)
} else {
// 如果没有 setter,在开发环境发出警告
warn('Write operation failed: computed value is readonly')
}
}set 访问器只负责执行用户提供的 setter 函数。setter 函数内部通常会去修改其他的 ref 或 reactive 对象,从而触发它们自己的响应式更新。
总结
computed 是 Vue 中一个基于懒执行和缓存的响应式 ref。
- 懒执行:
getter函数只在get value()被调用且被标记为“脏”(_dirty)时才执行。 - 缓存:
_dirty标记充当缓存开关。只要依赖不变,_dirty始终为false,get value()会直接返回已缓存的_value。 - 缓存失效:
computed内部的effect订阅了依赖(如count)。当依赖变化时,effect的scheduler会被触发。 scheduler的职责:- 设置
this._dirty = true(使缓存失效)。 - 调用
this.dep.trigger(),通知订阅computed的effect(如render)更新。
- 设置
- 链式更新:
render更新时会再次get value(),此时computed发现_dirty为true,才会重新计算。这个机制保证了计算值只在被需要时才更新。
