在前面的几篇文章中,我们完整地探索了 Vue3 响应式系统的核心:从
reactive和ref创建响应式数据,到track收集依赖,再到trigger触发更新。今天,我们将站在这些基础之上,剖析一个更高阶的响应式 API——computed,它如何在实现懒计算的同时,又能精确地响应变化。
前言:为什么需要 computed?
在模板中直接写复杂逻辑,往往会让代码变得臃肿且难以维护:
<template>
<div>{{ todos.filter(todo => todo.done).length }} / {{ todos.length }} 已完成</div>
</template>
我们可以用 methods 来封装:
methods: {
completedCount() {
console.log('methods 重新计算了');
return this.todos.filter(todo => todo.done).length;
}
}
但 methods 有个问题:每次渲染都会重新执行,即使 todos 没有变化。这就是 computed 登场的地方:
computed: {
completedCount() {
console.log('computed 重新计算了');
return this.todos.filter(todo => todo.done).length;
}
}
computed 的核心概念
惰性求值(Lazy Evaluation)
惰性求值意味着:计算属性不会立即执行,而是在真正需要其值时才进行计算。
const todos = reactive([
{ text: '学习 Vue3', done: false },
{ text: '理解 computed', done: true }
]);
// 创建 computed,但此时不会执行 getter
const completedCount = computed(() => {
console.log('getter 执行了');
return todos.filter(todo => todo.done).length;
});
console.log('computed 已创建,但 getter 未执行');
// 输出: computed 已创建,但 getter 未执行
// 访问 .value 时,getter 才执行
console.log(completedCount.value);
// 输出: getter 执行了
// 输出: 1
脏检查机制(Dirty Flag)
Vue3 通过一个内部的 _dirty 标志来实现缓存控制:
- 初始状态:
_dirty = true,表示需要重新计算。 - 首次访问:执行
getter,缓存结果,_dirty = false。 - 依赖变化:
_dirty = true,但不立即重新计算。 - 再次访问:发现
_dirty = true,重新计算并更新缓存。
// 伪代码:脏检查机制
class ComputedRefImpl {
_dirty = true; // 默认脏,需要计算
_value; // 缓存的值
get value() {
if (this._dirty) {
this._value = this.getter(); // 重新计算
this._dirty = false; // 标记为干净
}
return this._value;
}
}
缓存策略的优势
这种缓存策略带来了显著的性能提升:
const todos = reactive([/* 大量数据 */]);
const completedCount = computed(() => {
// 即使 todos 有 10000 项,只要依赖不变就不会重新计算
return todos.filter(todo => todo.done).length;
});
// 多次访问,只计算一次
console.log(completedCount.value); // 计算
console.log(completedCount.value); // 缓存
console.log(completedCount.value); // 缓存
// 只有依赖变化后才重新计算
todos.push({ text: '新任务', done: true }); // 触发 _dirty = true
console.log(completedCount.value); // 重新计算
computed 与 effect 的关系
基于 effect 的实现
computed 本质上是一个特殊的 effect,但它有两个关键区别:
- 懒执行:不立即运行,只在读取 .value 时运行。
- 缓存结果:依赖不变时直接返回缓存值。
class ComputedRefImpl {
constructor(getter) {
this.getter = getter;
this._dirty = true;
this._value = undefined;
// 创建一个 effect 来追踪依赖
this.effect = new ReactiveEffect(getter, () => {
// 调度器:依赖变化时执行
if (!this._dirty) {
this._dirty = true;
// 触发依赖这个 computed 的 effect
trigger(this, 'value');
}
});
}
get value() {
// 收集依赖(谁在读取这个 computed)
track(this, 'value');
if (this._dirty) {
this._dirty = false;
// 运行 effect 获取新值
this._value = this.effect.run();
}
return this._value;
}
}
双向依赖关系
computed 同时扮演着两个角色:
- 作为依赖的消费者:依赖其他响应式数据。
- 作为依赖的提供者:被其他 effect(如渲染函数)依赖。
const price = ref(100);
const quantity = ref(2);
// computed 作为消费者:依赖 price 和 quantity
const total = computed(() => price.value * quantity.value);
// effect 作为消费者:依赖 computed
effect(() => {
console.log(`总价是:${total.value}`);
});
这种双向关系通过 track 和 trigger 完美串联:
computed 的 setter 处理
可写计算属性
除了只读的计算属性,Vue3 还支持提供 setter 的可写计算属性:
const firstName = ref('张');
const lastName = ref('三');
const fullName = computed({
get: () => `${firstName.value}-${lastName.value}`,
set: (newValue) => {
[firstName.value, lastName.value] = newValue.split('-');
}
});
console.log(fullName.value); // 输出: 张-三
fullName.value = '李-四';
console.log(firstName.value); // 输出: 李
console.log(lastName.value); // 输出: 四
setter 的实现原理
支持 setter 的 computed 需要额外处理:
class ComputedRefImpl {
constructor(getter, setter) {
this.getter = getter;
this.setter = setter;
this._dirty = true;
this._value = undefined;
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 函数
现在让我们整合所有概念,实现一个完整的 computed:
class ComputedRefImpl {
constructor(getter, setter) {
this.getter = getter;
this.setter = setter;
this._dirty = true;
this._value = undefined;
this.__v_isRef = true; // 标记为 ref,支持自动解包
// 创建 effect,并传入调度器
this.effect = new ReactiveEffect(getter, () => {
// 调度器:依赖变化时执行
if (!this._dirty) {
this._dirty = true;
// 触发依赖这个 computed 的 effect
trigger(this, 'value');
}
});
}
get value() {
// 收集读取当前 computed 的依赖
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 实现
* @param {Function|Object} getterOrOptions 可以是 getter 函数,或者包含 get/set 的对象
* @returns {ComputedRefImpl} 计算属性 ref
*/
function computed(getterOrOptions) {
let getter;
let setter;
// 处理参数:可以是函数,也可以是对象
if (isFunction(getterOrOptions)) {
getter = getterOrOptions;
setter = null;
} else {
getter = getterOrOptions.get;
setter = getterOrOptions.set;
// 确保 getter 存在
if (!isFunction(getter)) {
console.warn('计算属性必须提供 getter 函数');
getter = () => undefined;
}
}
return new ComputedRefImpl(getter, setter);
}
对比:methods 与 computed 的本质区别
使用场景对比
| 特性 | methods | computed |
|---|---|---|
| 缓存 | 无缓存 | 有缓存 |
| 执行时机 | 每次访问/渲染 | 依赖变化时 |
| 适用场景 | 事件处理、非响应式逻辑 | 派生数据、复杂计算 |
| 返回值 | 任意类型 | 只能返回计算值 |
| 依赖追踪 | 无 | 自动追踪 |
| 性能开销 | 每次执行 | 首次 + 依赖变化 |
进阶:计算属性的最佳实践
1. 保持计算属性的纯粹性
计算属性应该是纯函数:相同的输入总是返回相同的输出,不产生副作用。
// ❌ 不纯粹:修改外部状态
const wrong = computed(() => {
externalVariable.value++; // 副作用!
return count.value * 2;
});
// ❌ 不纯粹:异步操作
const wrongAsync = computed(async () => {
return await fetchData(); // computed 不应返回 Promise
});
// ✅ 纯粹:只基于依赖计算
const right = computed(() => {
return count.value * 2;
});
2. 避免在 computed 中修改依赖
// ❌ 错误:在 computed 中修改依赖
const counter = ref(0);
const double = computed(() => {
counter.value++; // 会导致无限循环!
return counter.value * 2;
});
// ✅ 正确:使用 watch 来处理修改
watch(counter, (newVal) => {
console.log('counter 变化了', newVal);
});
3. 合理拆分计算属性
// ❌ 一个 computed 做太多事
const userInfo = computed(() => {
const fullName = `${user.firstName} ${user.lastName}`;
const age = new Date().getFullYear() - user.birthYear;
const status = user.isActive ? '在线' : '离线';
return { fullName, age, status };
});
// ✅ 拆分成多个 focused 的计算属性
const fullName = computed(() => `${user.firstName} ${user.lastName}`);
const age = computed(() => new Date().getFullYear() - user.birthYear);
const status = computed(() => user.isActive ? '在线' : '离线');
4. 使用 computed 的 setter 实现双向绑定
// 在组件中实现 v-model 双向绑定
const props = defineProps(['modelValue']);
const emit = defineEmits(['update:modelValue']);
const value = computed({
get: () => props.modelValue,
set: (newValue) => emit('update:modelValue', newValue)
});
// 模板中使用
<input v-model="value" />
5. 性能优化:避免不必要的计算
// ❌ 每次访问都创建新对象
const userProfile = computed(() => ({
name: user.value.name,
age: user.value.age,
email: user.value.email
}));
// ✅ 缓存结果,只有依赖变化才重新创建
import { computed } from 'vue';
const userProfile = computed(() => {
// 返回稳定的引用
return {
name: user.value.name,
age: user.value.age,
email: user.value.email
};
});
// 或者在需要时才创建
const userName = computed(() => user.value.name);
const userAge = computed(() => user.value.age);
const userEmail = computed(() => user.value.email);
结语
computed 是 Vue3 响应式系统中一颗璀璨的明珠,理解 computed 的实现原理,不仅能帮助我们更好地使用它,还能在遇到性能问题时做出正确的优化决策。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!