computed 的作用是创建一个带缓存的响应式值,只有依赖变化时才重新计算。 你可能要问了,effect 不是已经能响应式更新了吗?为什么还要 computed?
其实问题就在这里。effect 确实能响应式更新,但它有个问题:每次依赖变化,effect 里的函数都会重新执行。如果这个函数里有个很耗时的计算,比如遍历一个大数组,那每次执行都会重新算一遍,太浪费了。
computed 就聪明多了,它会缓存计算结果。只有依赖真的变了,才会重新计算。如果依赖没变,直接返回缓存的值,性能好很多。
你可能会想,那我用 effect 加个变量缓存不就行了?比如这样:
1 2 3 4 5 6 7 8 9
| let cached = null; let lastDeps = null;
effect(() => { if (depsChanged()) { cached = expensiveCompute(); } });
|
确实可以,但 computed 帮你把这个过程封装好了,用起来更方便,而且还能处理很多边界情况。
先看个简单的例子,感受一下 computed 的效果:
1 2 3 4 5 6 7 8 9 10 11 12 13
| import { ref, computed, effect } from "vue";
const count = ref(0); const doubleCount = computed(() => count.value * 2);
effect(() => { console.log('doubleCount:', doubleCount.value); });
setInterval(() => { count.value++; }, 1000);
|
运行之后,你会看到控制台每秒打印一次,数字从 0 开始递增(0, 2, 4, 6…)。注意这里 doubleCount 是个 computed,访问它的值要用 .value,因为它返回的也是个 ref。
为什么需要 computed?
effect 每次依赖变化都会执行,但有些计算很昂贵,我们希望缓存结果。比如:
1 2 3 4 5 6 7 8 9 10 11 12
| const list = ref([1, 2, 3, 4, 5]);
let sum = 0; effect(() => { sum = list.value.reduce((a, b) => a + b, 0); });
const sum = computed(() => { return list.value.reduce((a, b) => a + b, 0); });
|
如果 list 很长,或者计算很复杂,computed 的优势就明显了。
computed 的核心思路
computed 的实现其实挺巧妙的:
- 用 effect 收集依赖:computed 内部也用 effect 来收集依赖,这样依赖变化时能知道
- 用 dirty 标记控制计算:只有依赖变化时才标记为 dirty,访问时才重新计算
- 返回 ref:computed 返回的也是个 ref,可以像 ref 一样用
.value 访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| const doubleCount = computed(() => count.value * 2);
const doubleCount = { _value: undefined, _dirty: true, get value() { if (this._dirty) { this._value = count.value * 2; this._dirty = false; } return this._value; } };
|
但实际实现会更复杂,因为要处理依赖收集、缓存失效等。
模块化实现
在之前的响应式系统基础上,我们新增一个 computed.js 文件:
1 2 3 4 5 6 7 8
| reactivity/ ├── effect.js // ReactiveEffect / effect / scheduler ├── track.js // track / trigger / targetMap ├── reactive.js // reactive / Proxy ├── handlers.js // mutableHandlers ├── ref.js // ref / isRef / unref / toRef / toRefs ├── computed.js // computed / ComputedRefImpl └── index.js // 使用示例
|
reactivity/computed.js
内容与功能说明:
ComputedRefImpl 类:封装 computed 的逻辑,包含 getter、setter(可选)、缓存值、dirty 标记
dirty 标记:控制是否需要重新计算,依赖变化时通过 scheduler 标记为 dirty
_value 缓存:存储计算结果,避免重复计算
computed(getterOrOptions):支持函数形式(只有 getter)和对象形式(getter + setter)
- 返回 ref:computed 返回的是 ref,可以像 ref 一样使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| import { ReactiveEffect } from './effect.js'; import { ref } from './ref.js'; import { track, trigger } from './track.js';
export class ComputedRefImpl { constructor(getter, setter) { this._value = undefined; this._dirty = true; this._getter = getter; this._setter = setter; this._effect = new ReactiveEffect(getter, () => { if (!this._dirty) { this._dirty = true; trigger(this, 'value'); } }); }
get value() { track(this, 'value'); if (this._dirty) { this._dirty = false; this._value = this._effect.run(); } return this._value; }
set value(newValue) { if (this._setter) { this._setter(newValue); } else { console.warn('Computed property is readonly'); } } }
export function computed(getterOrOptions) { let getter; let setter;
if (typeof getterOrOptions === 'function') { getter = getterOrOptions; } else { getter = getterOrOptions.get; setter = getterOrOptions.set; }
return new ComputedRefImpl(getter, setter); }
|
更新 reactivity/track.js
为了让 computed 也能被 track,我们需要确保 targetMap 能处理 computed 实例:
实际上 track.js 不需要修改,因为 computed 返回的对象也可以被 track。
使用示例
1) 基本用法(只有 getter)
1 2 3 4 5 6 7 8 9 10 11
| import { ref, computed, effect } from './computed.js';
const count = ref(0); const doubleCount = computed(() => count.value * 2);
effect(() => { console.log('doubleCount:', doubleCount.value); });
count.value = 1; count.value = 2;
|
2) 带 setter 的 computed
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import { ref, computed } from './computed.js';
const firstName = ref('张'); const lastName = ref('三');
const fullName = computed({ get() { return firstName.value + lastName.value; }, set(newValue) { const names = newValue.split(''); firstName.value = names[0] || ''; lastName.value = names.slice(1).join('') || ''; } });
console.log(fullName.value);
fullName.value = '李四'; console.log(firstName.value); console.log(lastName.value);
|
3) computed 依赖其他 computed
1 2 3 4 5 6 7 8 9
| import { ref, computed } from './computed.js';
const count = ref(0); const doubleCount = computed(() => count.value * 2); const quadrupleCount = computed(() => doubleCount.value * 2);
console.log(quadrupleCount.value); count.value = 1; console.log(quadrupleCount.value);
|
4) computed 与 ref/reactive 配合
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import { reactive, computed, effect } from './computed.js';
const state = reactive({ items: [1, 2, 3, 4, 5], filter: 'even' });
const filteredItems = computed(() => { if (state.filter === 'even') { return state.items.filter(item => item % 2 === 0); } return state.items; });
effect(() => { console.log('过滤后的项:', filteredItems.value); });
state.items.push(6); state.filter = 'odd';
|
一些细节
computed 为什么需要缓存?
computed 的核心优势就是缓存。如果每次访问都重新计算,那和直接写个函数调用没区别了。缓存的好处是:
- 性能优化:避免重复计算
- 一致性:同一时刻多次访问返回相同值
- 懒计算:只有访问时才计算,不访问就不算
dirty 标记的作用
dirty 标记是 computed 缓存机制的关键:
dirty = true:表示需要重新计算
dirty = false:表示可以使用缓存值
依赖变化时,通过 scheduler 把 dirty 设为 true,但不会立即计算。等下次访问 .value 时,发现 dirty 是 true,才重新计算。
computed 和 effect 的区别
| 特性 |
effect |
computed |
| 执行时机 |
依赖变化立即执行 |
依赖变化标记 dirty,访问时才计算 |
| 返回值 |
无返回值 |
返回 ref |
| 缓存 |
无缓存 |
有缓存 |
| 用途 |
副作用(DOM 更新、日志等) |
派生状态(计算值) |
computed 的懒计算特性
computed 是懒计算的,也就是说:
- 如果没人访问
.value,即使依赖变了也不会计算
- 只有访问
.value 时,才会检查 dirty 并决定是否重新计算
这个特性在某些场景下很有用,比如一个 computed 依赖很多数据,但可能不会用到,懒计算就能避免不必要的计算。
小结
computed 的实现其实挺巧妙的,核心就是用 effect 收集依赖,用 dirty 标记控制缓存,返回 ref 方便使用。
computed(getter) 创建一个带缓存的响应式值
- 只有依赖变化时才标记为 dirty,访问时才重新计算
- 返回的是 ref,可以像 ref 一样用
.value 访问
- 支持 getter + setter,可以双向绑定
配合之前的 reactive、effect 和 ref,现在我们已经有了一个相当完整的响应式系统了。computed 让派生状态的管理变得简单高效,是 Vue3 响应式系统的重要一环。