在Vue3响应式体系中,computed(计算属性)是处理“依赖推导值”的核心API,其设计初衷是在保证响应式联动的同时,通过惰性执行和缓存机制优化性能,同时支持getter/setter双写法适配只读、可写场景。相比普通方法和effect副作用函数,computed更贴合“数据推导”的语义,也是日常开发中提升代码效率与性能的关键工具。本文将从特性原理、写法实战、避坑要点三个维度,彻底讲透computed的核心逻辑与最佳实践。
一、核心特性:惰性执行与缓存机制的协同逻辑
computed的两大核心特性——惰性执行和缓存机制,并非孤立存在,而是协同工作以实现“按需计算、复用结果”的目标,这也是它区别于组件方法、effect的核心优势。
1. 惰性执行:不访问不计算,拒绝无意义开销
computed的“惰性”体现在两个关键节点,彻底规避无效计算:
- 初始化时不执行:创建computed对象时,其内部的计算逻辑(getter函数)不会立即执行,仅在首次访问
.value时才触发第一次计算。这与effect不同——effect在初始化时会主动执行一次,若计算逻辑复杂,可能造成初始化性能损耗。 - 依赖变化时不立即执行:当computed依赖的响应式数据发生变化时,它不会同步重新计算,仅会标记为“脏数据(dirty)”;只有当再次访问
.value时,才会触发重新计算并更新结果。
通俗类比:computed就像一位“按需工作的计算器”,不会主动提前计算结果,也不会在数据源变动后立刻重新计算,只有当你主动索取结果(访问.value)时,才会根据数据源是否变化,决定是返回缓存结果还是重新计算。
2. 缓存机制:依赖不变,结果复用
缓存机制是computed性能优化的核心,其逻辑围绕“脏数据标记”展开,流程如下:
- 首次计算缓存:首次访问computed.value时,执行getter函数计算结果,将结果缓存到内部变量中,并标记为“非脏数据(!dirty)”。
- 依赖不变复用缓存:后续多次访问computed.value时,若依赖的响应式数据未变化(仍为!dirty),则直接返回缓存结果,不执行getter函数,避免重复计算。
- 依赖变化标记脏数据:当依赖的响应式数据发生变化时,computed通过响应式系统的trigger机制,将自身标记为“脏数据(dirty)”,但不立即计算。
- 再次访问重新计算:当标记为dirty后再次访问computed.value时,重新执行getter函数计算新结果,更新缓存,并重置为!dirty状态。
import { ref, computed } from 'vue';
const a = ref(1);
const b = ref(2);
// 定义computed,依赖a和b
const sum = computed(() => {
console.log('执行计算逻辑');
return a.value + b.value;
});
// 首次访问:执行计算,缓存结果(sum=3)
console.log(sum.value); // 输出:执行计算逻辑 → 3
// 再次访问:依赖未变,复用缓存
console.log(sum.value); // 输出:3(无计算日志)
// 修改依赖,标记为脏数据
a.value = 2;
// 未访问.value,不执行计算
console.log('依赖已修改,但未访问sum');
// 再次访问:依赖变化,重新计算(sum=4)
console.log(sum.value); // 输出:执行计算逻辑 → 4
缓存的价值:对于计算逻辑复杂(如循环遍历、数据转换)、访问频繁的场景,缓存机制能大幅减少计算开销,提升组件渲染性能——这是普通方法无法实现的(普通方法每次调用都会重新执行)。
3. 与组件方法、effect的核心区别
为更清晰理解computed的特性,对比三者的核心差异:
| 特性 | computed | 组件方法 | effect |
|---|---|---|---|
| 执行时机 | 惰性执行(访问.value时) | 调用时执行(每次调用都重新计算) | 初始化执行,依赖变化同步执行 |
| 缓存机制 | 有(依赖不变复用结果) | 无(无缓存,重复调用重复计算) | 无(依赖变化必执行副作用) |
| 核心用途 | 响应式数据推导(只读/可写) | 通用逻辑封装(非响应式推导) | 响应式副作用处理(DOM操作、异步请求) |
二、实战写法:getter/setter 适配不同场景
computed支持两种写法:只读写法(仅getter) 和可写写法(getter+setter) ,分别适配“仅推导值,不允许修改”和“既推导值,又允许手动修改”的场景。
1. 只读写法(默认):仅传递getter函数
这是最常用的写法,适用于仅需要通过依赖推导值,不允许手动修改computed结果的场景。此时computed返回一个只读的Ref对象,修改其.value会被拦截(开发环境抛出警告)。
import { ref, computed } from 'vue';
const firstName = ref('张');
const lastName = ref('三');
// 只读computed:推导全名
const fullName = computed(() => {
return `${firstName.value}${lastName.value}`;
});
console.log(fullName.value); // 输出:张三
// 尝试修改只读computed(开发环境抛警告,修改无效)
fullName.value = '李四';
适用场景:模板渲染依赖的推导值(如拼接字符串、计算列表总数、格式化日期)、组件内部的只读数据推导等。
2. 可写写法:传递getter+setter对象
当需要手动修改computed结果,并同步更新依赖的响应式数据时,可传递一个包含get和set方法的对象,实现可写computed。
get方法:与只读写法一致,负责根据依赖推导computed值。set方法:接收手动修改的值,负责反向更新依赖的响应式数据,实现“修改computed值 → 同步更新数据源”的联动。
import { ref, computed } from 'vue';
const firstName = ref('张');
const lastName = ref('三');
// 可写computed:get推导值,set反向更新数据源
const fullName = computed({
// get:推导全名
get() {
return `${firstName.value}${lastName.value}`;
},
// set:接收手动修改的fullName,拆分后更新firstName和lastName
set(newFullName) {
const [newFirst, newLast] = newFullName.split('');
firstName.value = newFirst;
lastName.value = newLast;
}
});
console.log(fullName.value); // 输出:张三
// 手动修改computed值,触发set方法
fullName.value = '李四';
// 依赖的数据源同步更新
console.log(firstName.value); // 输出:李
console.log(lastName.value); // 输出:四
// get方法重新推导,结果同步
console.log(fullName.value); // 输出:李四
适用场景:需要双向绑定的计算属性(如表单中的组合值、可编辑的推导字段),手动修改computed值时,需同步更新底层数据源的场景。
注意:可写computed的核心是“反向更新数据源”,set方法中必须修改依赖的响应式数据,否则computed值会与数据源不一致,导致响应式异常。
三、避坑指南:这些细节决定computed使用效果
1. 避免在computed中执行副作用
computed的设计语义是“数据推导”,而非“副作用处理”。若在getter函数中执行DOM操作、异步请求、修改其他响应式数据等副作用,会导致:
- 副作用执行时机不可控(因惰性执行,依赖变化后不立即执行);
- 重复执行或遗漏执行(因缓存机制,依赖不变时不执行getter);
- 代码逻辑混乱,难以调试。
正确做法:副作用逻辑应放在effect、watch或组件方法中,computed仅负责纯数据推导。
2. 依赖必须是响应式数据
computed的缓存和响应式更新,依赖于对响应式数据的追踪(track/trigger)。若getter函数中依赖的是非响应式数据,computed将无法感知数据变化,始终返回缓存结果,导致响应式失效。
import { computed } from 'vue';
// 非响应式数据
let a = 1;
const sum = computed(() => a + 2);
console.log(sum.value); // 输出:3
// 修改非响应式数据,computed无法感知,仍返回缓存
a = 2;
console.log(sum.value); // 输出:3(响应式失效)
3. 可写computed避免循环依赖
在可写computed的set方法中,若直接修改自身的.value,会触发get方法重新计算,进而可能再次触发set,形成循环依赖,导致栈溢出。
// 错误示例:循环依赖
const num = ref(1);
const wrongComputed = computed({
get() {
return num.value * 2;
},
set(newVal) {
// 直接修改自身,触发循环
wrongComputed.value = newVal / 2;
}
});
正确做法:set方法中仅修改依赖的底层响应式数据,不直接操作computed自身。
4. 复杂计算优先用computed
对于模板中频繁访问的复杂计算逻辑,优先使用computed而非组件方法——组件方法每次模板渲染都会重新执行,而computed通过缓存机制仅在依赖变化时重新计算,能显著提升渲染性能。
四、总结
computed的核心价值的是“高效的响应式数据推导”:通过惰性执行避免无意义的提前计算,通过缓存机制减少重复计算开销,同时借助getter/setter双写法适配只读、可写全场景。
使用computed的关键原则:坚守“数据推导”的语义,不混入副作用,依赖响应式数据,可写场景中确保反向更新底层数据源。掌握这些逻辑,能让computed成为优化代码性能、提升开发效率的利器,在复杂项目中发挥更大价值。