computed 是 Vue 响应式系统中一个强大而优雅的特性,它允许我们基于响应式数据派生出新的值,并且具备惰性求值(Lazy Evaluation) 和缓存的特性。理解 computed 为何不会立即执行,以及其背后的 effect 的 lazy 选项,是掌握 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: true:effect创建后不立即执行,只有当被显式调用时才执行。
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;
}
};
}关键点解析:
lazy: true:computed创建时,其内部的effect不会立即执行。- 因此,
getter函数(() => count.value * 2)不会在定义时运行。
scheduler:- 当
computed依赖的数据(如count.value)发生变化时,trigger会通知这个effect。 - 但由于
lazy: true,effect不会立即重新计算。 - 取而代之的是,
scheduler被调用,将dirty标记为true,表示“结果已过期,需要重新计算”。
- 当
get value():- 当我们访问
doubled.value时,会触发get拦截器。 - 如果
dirty为true,则手动调用effect()执行getter函数,重新计算值,并将dirty设为false。 - 如果
dirty为false,直接返回缓存的value,不会重新执行getter。
- 当我们访问
三、惰性求值的完整生命周期
以 doubled = computed(() => count.value * 2) 为例:
1. 创建阶段
js
const doubled = computed(() => count.value * 2);- 创建
lazy: true的effect。 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;count的set拦截器触发trigger。trigger找到依赖count的effect(即doubled的内部effect)。- 调用
scheduler→dirty = true。 - 注意:此时
getter函数并未执行,只是标记为“脏”。
4. 再次访问
js
console.log(doubled.value); // 2- 访问
doubled.value。 - 发现
dirty === true。 - 执行
effect()→getter()→ 重新计算count.value * 2。 - 结果
2赋值给value。 dirty = false。- 返回
value。
四、为什么需要惰性求值?
性能优化:
- 避免不必要的计算。如果一个
computed值从未被使用,就不应该浪费 CPU 去计算它。 - 例如,在组件中定义了一个复杂的
computed,但模板中未引用它,那么它永远不会执行。
- 避免不必要的计算。如果一个
缓存机制的基础:
- 惰性求值与
dirty标记结合,实现了结果缓存。 - 只要依赖未变,多次访问
computed.value都会返回缓存值,不会重复计算。
- 惰性求值与
符合“按需”原则:
- 响应式系统应该只在需要时才工作,
computed的惰性求值完美体现了这一点。
- 响应式系统应该只在需要时才工作,
五、与 watchEffect 的对比
| 特性 | computed | watchEffect |
|---|---|---|
| 执行时机 | 惰性(访问时执行) | 立即执行 |
| 返回值 | Ref(可访问的值) | 无返回值(副作用) |
| 用途 | 派生数据 | 副作用(如日志、API 调用) |
lazy 选项 | true | false(默认) |
watchEffect 本质上是一个 lazy: false 的 effect,创建后立即执行。
六、总结
computed 的惰性求值机制可以归结为:
lazy: true:computed内部的effect在创建时不立即执行。scheduler:当依赖变化时,不重新计算,而是将dirty标记为true。get value():只有在访问value时,才检查dirty标志,决定是否重新执行getter。
这种设计使得 computed:
- 高效:避免了不必要的计算。
- 智能:仅在需要时才工作。
- 可缓存:结果被记忆,直到依赖变化。
理解 lazy 选项,就是理解 computed 如何在“响应式”和“性能”之间取得完美平衡。