[响应式原理03]:computed 的缓存机制底层是怎么实现的?

4 阅读4分钟

computed 的"缓存"特性几乎每个 Vue 开发者都会说,但问到底层是怎么实现的,大部分人就卡了。这篇文章把 dirty flagscheduler 这两个核心机制讲清楚,面试时不只是会说"有缓存",而是能说清楚"为什么有缓存"。


第三轮提问(追问):

"你知道 computed 有缓存。但我想问的是底层:

  1. 这个缓存是怎么维护的?依赖变了之后,computed 是立即重新计算,还是等到有人来取值才重新计算?
  2. Vue3 中有一个叫 scheduler(调度器) 的概念,computed 是怎么利用这个机制来实现"懒计算"的?"

参考答案

一、computed 的本质:一个带有特殊 effect 的对象

在 Vue3 源码中,computed(() => price * quantity) 并不是一个简单的函数调用,而是创建了一个 ComputedRefImpl 实例。这个对象上有两个核心属性:

class ComputedRefImpl {
  public _value: any        // 上一次计算的缓存值
  public _dirty: boolean = true  // 脏位标记:true 表示"需要重新计算"
  // ...
}

true 初始为脏(_dirty = true),代表"我还没算过,需要计算"。


二、关键机制:scheduler 接管了依赖变化的通知

通常情况下,当一个 effect(副作用)依赖的响应式数据变化时,框架会立即重新执行这个 effect

computed 不能这么做——如果模板还没读取 computed 的值,贸然计算一遍是浪费的。

Vue3 的做法是给 computed 内部的 effect 配置一个自定义 scheduler。有了 scheduler,依赖变化时就不再直接执行 effect 了,而是转为执行这个 scheduler 函数。

computedscheduler 只做两件事:

scheduler() {
  if (!this._dirty) {
    this._dirty = true         // 1. 把自己标记为"脏"(需要重新计算)
    triggerRefValue(this)      // 2. 通知"用了这个 computed 的地方"去更新
  }
}

注意:这里没有重新计算,只是打了个标记,然后通知下游。


三、完整的"懒计算"流程

把整个链路串起来:

响应式数据变化(如 price 改变)
  ↓
触发 computed 内部 effect 的 scheduler
  ↓
scheduler:把 _dirty 设为 true,通知模板需要更新
  ↓
Vue 调度器将页面更新任务加入队列
  ↓
渲染时模板访问 computed.value
  ↓
触发 computed 的 get value() 拦截器
  ↓
get value() 里检查:if (this._dirty)
  → 是脏的:执行 getter,拿到新值,存入 _value,_dirty = false
  ↓
返回 _value 给模板

关键点:getter 函数(实际的计算逻辑)只在真正有人来取值时才执行,这就是"懒计算"。

如果 price 连续改了 100 次,但模板只重新渲染了 1 次,getter 只会被执行 1 次。


四、经典坑:以为"依赖不变就不重算",其实是"脏位没被置 true"

很多同学理解 computed 缓存的方式是:"依赖没变,就用缓存值"——这个描述在表面上没错,但容易造成一个误解。

实际的机制是反向的

  • 初始 _dirty = true,强制第一次取值时计算。
  • 依赖变化时,_dirty 被置为 true(打脏),下次取值重新计算,计算完 _dirty = false
  • 依赖没变,_dirty 保持 false,取值时直接返回 _value,不执行 getter。

所以缓存的本质,是 _dirty = false 时直接返回 _value,而不是"检测依赖有没有变"。Vue 从不主动去"比较依赖",它只被动地等待依赖告诉它"我变了"。


五、scheduler 的更大意义(升维视角)

scheduler 不只是 computed 在用。Vue3 整个更新调度系统都依赖这个机制——当响应式数据变化时,组件的重新渲染也是通过 scheduler 把更新任务推入一个微任务队列,而不是立即执行。这就是为什么 nextTick 能等到 DOM 更新完之后再执行回调。

能在面试中把 computed 的缓存和 Vue 整体调度机制联系起来说,就是降维打击了。


复盘:三句话记住核心

  1. _dirty = true → 需要重算;_dirty = false → 直接返回缓存
  2. 依赖变化时,scheduler 只打"脏"标记 + 通知下游,不立即计算
  3. 真正的计算推迟到 get value() 被访问时——这就是懒计算

下一篇:[[响应式原理04]:ref / reactive 能完全替代 Pinia 吗?什么情况下应该选 Pinia?]