Skip to content

computed:计算属性的懒执行与缓存原理

在 Vue 的响应式系统中,reactiveref 提供了可变的状态。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。理解它的关键在于它扮演两个角色

  1. 作为“订阅者”:它内部的 effect 会“订阅”它所依赖的数据(如 count)。
  2. 作为“发布者”:它本身是一个 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 的逻辑实现了懒执行缓存

  • 只有在 _dirtytrue 时,才执行 this.effect.run()(即 getter 函数)。
  • 否则,无论访问多少次,都只返回已缓存的 _value

缓存失效 (scheduler):依赖如何通知 computed

_dirty 标记(缓存)是如何失效的?这依赖于 constructor 中定义的 scheduler(调度器)。

一个完整的更新流程如下:

  1. 创建const count = ref(1)const doubled = computed(() => count.value * 2)

    • doubled 被创建。_dirty = truedoubled.effect 被创建,但未运行
  2. 首次访问(render 执行)render 访问 doubled.value,触发 doubled.get value()

    • doubled.dep.track()render 开始订阅 doubled
    • if (this._dirty):检查到 _dirtytrue
    • this._dirty = false:标记为“干净”(缓存有效)。
    • this.effect.run():执行 getter()
    • run() 内部,count.value 被访问,触发 count.dep.track()
    • doubled.effect 订阅count
    • _value 被缓存为 2
    • get value() 返回 2
  3. 依赖变化count.value = 3 执行。

    • countset value() 被触发。
    • count.dep.trigger() 被调用。
    • trigger 找到了 count 的订阅者,即 doubled.effect
    • trigger 调用 doubled.effect.scheduler()(而不是 run())。
  4. scheduler 执行

    • computedscheduler 被执行。
    • this._dirty = true“脏”标记被设置(缓存失效)!
    • this.dep.trigger()doubled 通知它自己的订阅者(即 render):“我的值可能变了,你需要重新运行!”
  5. 二次访问(render 重新执行)

    • render 重新运行,再次访问 doubled.value,触发 doubled.get value()
    • doubled.dep.track()render 再次订阅 doubled
    • if (this._dirty)检查到 _dirtytrue
    • this._dirty = false:标记为“干净”。
    • this.effect.run()重新执行 getter
    • ... (重新订阅 count) ...
    • _value 被缓存为 6
    • get value() 返回 6

可写计算属性 (set value)

如果 computed 接收的是一个带 getset 的对象,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 函数内部通常会去修改其他的 refreactive 对象,从而触发它们自己的响应式更新。

总结

computed 是 Vue 中一个基于懒执行缓存的响应式 ref

  • 懒执行getter 函数只在 get value() 被调用被标记为“脏”(_dirty)时才执行。
  • 缓存_dirty 标记充当缓存开关。只要依赖不变,_dirty 始终为 falseget value() 会直接返回已缓存的 _value
  • 缓存失效computed 内部的 effect 订阅了依赖(如 count)。当依赖变化时,effectscheduler 会被触发。
  • scheduler 的职责
    1. 设置 this._dirty = true(使缓存失效)。
    2. 调用 this.dep.trigger(),通知订阅 computedeffect(如 render)更新。
  • 链式更新render 更新时会再次 get value(),此时 computed 发现 _dirtytrue,才会重新计算。这个机制保证了计算值只在被需要时才更新。

微信公众号二维码

Last updated: