Skip to content

computed 是 Vue 响应式系统中一个强大而优雅的特性,它允许我们基于响应式数据派生出新的值,并且具备惰性求值(Lazy Evaluation)缓存的特性。理解 computed 为何不会立即执行,以及其背后的 effectlazy 选项,是掌握 Vue 响应式核心的关键。


一、现象:computed 为何不立即执行?

考虑以下代码:

js
import { ref, computed } from 'vue';

const count = ref(0);
const doubled = computed(() => {
  console.log('computed 执行了');
  return count.value * 2;
});

console.log(doubled.value); // 输出: 'computed 执行了', 然后 0
  • 我们定义了一个 computed,但它并没有在定义时立即打印 'computed 执行了'
  • 只有当我们第一次访问 doubled.value 时,它才执行。

这种“只有在被访问时才计算”的行为,就是惰性求值


二、核心机制:computed 背后的 effect

computed 的实现依赖于 Vue 响应式系统中的 effect(副作用函数)。每个 computed 实际上是一个特殊的 effect

1. effect 的基本概念

effect 是 Vue 响应式系统的核心。它封装了一个函数,当函数内访问的响应式数据变化时,该函数会自动重新执行。

js
effect(() => {
  console.log(count.value); // 依赖 count
});
// 当 count.value 改变时,此函数会重新执行

2. effect 的 lazy 选项

effect 函数接受一个配置对象,其中 lazy 选项是 computed 惰性求值的关键。

  • lazy: false(默认)effect 创建后立即执行一次。
  • lazy: trueeffect 创建后不立即执行,只有当被显式调用时才执行。
js
// 默认:立即执行
effect(() => console.log('Hello')); // 立即打印

// lazy: true:惰性执行
const runner = effect(() => console.log('Hello'), { lazy: true });
// 此时不会打印

runner(); // 手动调用,才会打印

3. computed 如何使用 lazy

computed 的内部实现大致如下:

js
function computed(getter) {
  let value;
  let dirty = true; // 标记是否需要重新计算

  // 创建一个 lazy 的 effect
  const effect = effect(getter, {
    lazy: true,
    scheduler: () => {
      // 当依赖变化时,标记为“脏”,但不立即计算
      dirty = true;
    }
  });

  // 返回一个 Ref 对象
  return {
    get value() {
      if (dirty) {
        // 只有在访问 value 且 dirty 为 true 时,才执行 effect
        value = effect();
        dirty = false;
      }
      return value;
    }
  };
}

关键点解析

  1. lazy: true

    • computed 创建时,其内部的 effect 不会立即执行。
    • 因此,getter 函数(() => count.value * 2)不会在定义时运行。
  2. scheduler

    • computed 依赖的数据(如 count.value)发生变化时,trigger 会通知这个 effect
    • 但由于 lazy: trueeffect 不会立即重新计算。
    • 取而代之的是,scheduler 被调用,将 dirty 标记为 true,表示“结果已过期,需要重新计算”。
  3. get value()

    • 当我们访问 doubled.value 时,会触发 get 拦截器。
    • 如果 dirtytrue,则手动调用 effect() 执行 getter 函数,重新计算值,并将 dirty 设为 false
    • 如果 dirtyfalse,直接返回缓存的 value不会重新执行 getter

三、惰性求值的完整生命周期

doubled = computed(() => count.value * 2) 为例:

1. 创建阶段

js
const doubled = computed(() => count.value * 2);
  • 创建 lazy: trueeffect
  • dirty = true
  • getter 函数未执行。

2. 首次访问

js
console.log(doubled.value); // 0
  • 访问 doubled.value
  • 发现 dirty === true
  • 执行 effect()getter()count.value * 2
  • getter 执行时,访问 count.value,建立依赖关系。
  • 计算结果 0 赋值给 value
  • dirty = false
  • 返回 value

3. 依赖变化

js
count.value = 1;
  • countset 拦截器触发 trigger
  • trigger 找到依赖 counteffect(即 doubled 的内部 effect)。
  • 调用 schedulerdirty = true
  • 注意:此时 getter 函数并未执行,只是标记为“脏”。

4. 再次访问

js
console.log(doubled.value); // 2
  • 访问 doubled.value
  • 发现 dirty === true
  • 执行 effect()getter() → 重新计算 count.value * 2
  • 结果 2 赋值给 value
  • dirty = false
  • 返回 value

四、为什么需要惰性求值?

  1. 性能优化

    • 避免不必要的计算。如果一个 computed 值从未被使用,就不应该浪费 CPU 去计算它。
    • 例如,在组件中定义了一个复杂的 computed,但模板中未引用它,那么它永远不会执行。
  2. 缓存机制的基础

    • 惰性求值与 dirty 标记结合,实现了结果缓存
    • 只要依赖未变,多次访问 computed.value 都会返回缓存值,不会重复计算。
  3. 符合“按需”原则

    • 响应式系统应该只在需要时才工作,computed 的惰性求值完美体现了这一点。

五、与 watchEffect 的对比

特性computedwatchEffect
执行时机惰性(访问时执行)立即执行
返回值Ref(可访问的值)无返回值(副作用)
用途派生数据副作用(如日志、API 调用)
lazy 选项truefalse(默认)

watchEffect 本质上是一个 lazy: falseeffect,创建后立即执行。


六、总结

computed 的惰性求值机制可以归结为:

  1. lazy: truecomputed 内部的 effect 在创建时不立即执行。
  2. scheduler:当依赖变化时,不重新计算,而是将 dirty 标记为 true
  3. get value():只有在访问 value 时,才检查 dirty 标志,决定是否重新执行 getter

这种设计使得 computed

  • 高效:避免了不必要的计算。
  • 智能:仅在需要时才工作。
  • 可缓存:结果被记忆,直到依赖变化。

理解 lazy 选项,就是理解 computed 如何在“响应式”和“性能”之间取得完美平衡。