computed 计算属性的实现

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);
});

// 每秒加1
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]);

// 用 effect,依赖变化时立即执行,无缓存
let sum = 0;
effect(() => {
sum = list.value.reduce((a, b) => a + b, 0); // list 变化时立即计算
});

// 用 computed,依赖变化时标记为 dirty,只有访问 .value 时才计算,有缓存
const sum = computed(() => {
return list.value.reduce((a, b) => a + b, 0); // 懒计算 + 缓存
});

如果 list 很长,或者计算很复杂,computed 的优势就明显了。

computed 的核心思路

computed 的实现其实挺巧妙的:

  1. 用 effect 收集依赖:computed 内部也用 effect 来收集依赖,这样依赖变化时能知道
  2. 用 dirty 标记控制计算:只有依赖变化时才标记为 dirty,访问时才重新计算
  3. 返回 ref:computed 返回的也是个 ref,可以像 ref 一样用 .value 访问
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// computed 的核心思路
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
// computed.js
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;

// 创建一个 effect 来收集依赖
// 注意:这里用了 scheduler,依赖变化时不会立即执行 getter
// 而是标记为 dirty,等下次访问时才重新计算
this._effect = new ReactiveEffect(getter, () => {
// scheduler:依赖变化时,只标记为 dirty,不立即计算
if (!this._dirty) {
this._dirty = true;
// 通知所有依赖这个 computed 的地方
trigger(this, 'value');
}
});
}

get value() {
// 收集依赖:访问 computed.value 时,收集当前 activeEffect
track(this, 'value');

// 如果 dirty,重新计算
if (this._dirty) {
this._dirty = false;
this._value = this._effect.run();
}

return this._value;
}

set value(newValue) {
// 如果有 setter,调用 setter
if (this._setter) {
this._setter(newValue);
} else {
console.warn('Computed property is readonly');
}
}
}

export function computed(getterOrOptions) {
let getter;
let setter;

// 支持两种形式:
// 1. computed(() => count.value * 2) // 函数形式
// 2. computed({ get: ..., set: ... }) // 对象形式
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 实例:

1
2
// track.js(无需修改,因为 computed 也是对象,可以被 track)
// 但要注意:computed 的 value 属性会被 track,这样其他 effect 就能依赖 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; // 触发 computed 重新计算,打印:doubleCount: 2
count.value = 2; // 触发 computed 重新计算,打印:doubleCount: 4

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); // 0
count.value = 1;
console.log(quadrupleCount.value); // 4

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); // 触发 computed,打印:[2, 4, 6]
state.filter = 'odd'; // 触发 computed,打印:[1, 3, 5]

一些细节

computed 为什么需要缓存?

computed 的核心优势就是缓存。如果每次访问都重新计算,那和直接写个函数调用没区别了。缓存的好处是:

  1. 性能优化:避免重复计算
  2. 一致性:同一时刻多次访问返回相同值
  3. 懒计算:只有访问时才计算,不访问就不算

dirty 标记的作用

dirty 标记是 computed 缓存机制的关键:

  • dirty = true:表示需要重新计算
  • dirty = false:表示可以使用缓存值

依赖变化时,通过 scheduler 把 dirty 设为 true,但不会立即计算。等下次访问 .value 时,发现 dirtytrue,才重新计算。

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 响应式系统的重要一环。