vue3-computed
computed(计算属性)是 Vue 3 中处理派生状态(Derived State)的核心利器。它的本质是“响应式数据的缓存计算器”。
结合你已经学过的watch和methods,我用“Excel 单元格公式”来类比:原始数据是A1和B1,计算属性就像=SUM(A1:B1)——只有当A1或B1变了,求和结果才会重新计算;否则直接读缓存。
下面我为你彻底拆解computed的语法、缓存机制、与watch/methods的本质区别,以及TypeScript 中的类型陷阱。
1. 基础语法(只读计算属性)
在组合式 API(<script setup>)中,computed接收一个getter 函数,返回一个不可变的Ref对象。
<script setup lang="ts"> import { ref, computed } from 'vue' // 原始数据 const firstName = ref('张') const lastName = ref('三') // 计算属性:返回全名 const fullName = computed(() => { return firstName.value + lastName.value }) // 模板中直接用,不需要 .value // 但在 JS 逻辑中访问需要 .value console.log(fullName.value) // 输出: "张三" </script> <template> <p>{{ fullName }}</p> <!-- 自动解包,显示 "张三" --> </template>核心特性:只要firstName或lastName不变,多次访问fullName.value会直接返回缓存结果,不会重新执行 getter 函数。
2. 🚀 进阶语法(可写计算属性)
虽然 90% 的场景计算属性是只读的,但你也可以定义一个setter,实现“修改派生值影响原始值”的效果(类似 Vuex 的commit)。
<script setup lang="ts"> const firstName = ref('张') const lastName = ref('三') const fullName = computed({ // getter:读取时触发 get: () => firstName.value + lastName.value, // setter:赋值时触发(比如 v-model 绑定) set: (newValue: string) => { const names = newValue.split(' ') firstName.value = names[0] || '' lastName.value = names[1] || '' } }) // 当执行 fullName.value = '李 四' 时,会触发 setter // 自动把 firstName 改为 '李',lastName 改为 '四' </script> <template> <!-- 双向绑定可写计算属性(非常优雅) --> <input v-model="fullName" /> </template>警告:setter 里绝对不能修改计算属性本身(即不能
fullName.value = xxx内部又触发 setter),这会造成死循环。
3.computedvsmethods(缓存机制的降维打击)
这是面试最常问的对比,我直接用代码演示性能差异:
<script setup lang="ts"> import { ref, computed } from 'vue' const count = ref(1) // 1. 计算属性(有缓存) const computedDouble = computed(() => { console.log('计算属性重新计算了!') return count.value * 2 }) // 2. 方法(无缓存,每次调用都重新执行) const methodDouble = () => { console.log('方法重新执行了!') return count.value * 2 } </script> <template> <div> <!-- 在模板中调用 3 次计算属性 --> <p>{{ computedDouble }}</p> <!-- 打印 1 次 "计算属性重新计算了!" --> <p>{{ computedDouble }}</p> <!-- 直接读缓存,不打印 --> <p>{{ computedDouble }}</p> <!-- 直接读缓存,不打印 --> <!-- 在模板中调用 3 次方法 --> <p>{{ methodDouble() }}</p> <!-- 打印 1 次 --> <p>{{ methodDouble() }}</p> <!-- 打印 1 次 --> <p>{{ methodDouble() }}</p> <!-- 打印 1 次 --> <!-- 总共打印 3 次! --> </div> </template>结论:
methods:适合事件处理或每次都需要重新计算的场景(如带参数的格式化)。computed:适合依赖响应式数据且计算开销较大的场景(如数组过滤、复杂数学计算)。
4.computedvswatch(本质不同)
| 对比维度 | computed | watch |
|---|---|---|
| 定位 | 派生新数据(输入 → 输出) | 执行副作用(输入 → 触发动作) |
| 是否有返回值 | ✅必须 return | ❌ 没有返回值(只执行逻辑) |
| 缓存 | ✅ 有缓存,依赖不变就不重新计算 | ❌ 无缓存,只要数据变化就执行回调 |
| 典型场景 | 过滤列表、格式化文本、拼接姓名 | 发 API 请求、打印日志、操作 DOM |
实战对比:
// ✅ 用 computed:根据搜索词过滤列表(派生新数组) const filteredList = computed(() => { return list.value.filter(item => item.includes(searchKey.value)) }) // ✅ 用 watch:搜索词变了,去后端拉新数据(执行副作用) watch(searchKey, (newKey) => { fetchListByKey(newKey) // 发请求 })5. TypeScript 类型标注(让 VSCode 精准提示)
computed在 TypeScript 中自动推导类型,但遇到复杂对象或联合类型时,需要显式标注。
① 自动推导(大多数情况够用)
const count = ref(1) const double = computed(() => count.value * 2) // double 被自动推断为 ComputedRef<number>② 显式泛型(处理可能返回null或undefined的情况)
// 假设返回可能是 User 对象或 null const user = ref<User | null>(null) // 显式声明返回类型,避免 TS 报错 const displayName = computed<string>(() => { if (user.value) { return user.value.name } return '未登录' // 保证总是返回 string })③ 可写计算属性的类型(Set 参数自动检查)
const fullName = computed<string>({ get: () => firstName.value + lastName.value, set: (newVal: string) => { // newVal 被限定为 string // ... } })6. ⚠️ 致命陷阱(新手必踩的 3 个坑)
陷阱 1:在计算属性里修改其他响应式数据(副作用)
// ❌ 绝对禁止! const count = ref(0) const double = computed(() => { count.value++ // 在 getter 里改了别的响应式数据!会触发无限循环 return count.value * 2 })原则:计算属性的 getter必须是无副作用的纯函数(只读,不写)。
陷阱 2:在模板中反复调用方法代替计算属性(导致性能浪费)
<!-- ❌ 不好:每次渲染都重新计算(且无法缓存) --> <p>{{ formatPrice(price) }}</p> <!-- ✅ 推荐:用计算属性自动缓存 --> <p>{{ formattedPrice }}</p>陷阱 3:忘记.value(尤其在script中)
const fullName = computed(() => firstName.value + lastName.value) // ❌ 错误:fullName 本身是一个 Ref 对象 console.log(fullName) // 输出 RefImpl 对象 // ✅ 正确:必须取 .value console.log(fullName.value) // 输出 "张三"7. 结合你之前的代码:给父组件添加计算属性
在你的父组件中,provideData有theme和version,你可以派生一个新状态:
const provideData = reactive({ theme: 'dark', version: '1.0.0', }) // 派生一个显示用的主题描述(只读) const themeDescription = computed(() => { return provideData.theme === 'dark' ? '🌙 暗色模式' : '☀️ 亮色模式' })然后在模板中用{{ themeDescription }}显示,当provideData.theme变化时,描述自动更新。
8. 性能优化进阶:computed的懒计算
computed是懒执行的:
- 即使依赖的数据变了,如果没有任何地方访问
computed的值,它不会立即重新计算。 - 只有当你真正去读
.value或模板渲染它时,它才会“迫不得已”重新计算。
这比
watch更高效:watch是数据一变立即执行回调,不管你用不用结果。
总结记忆口诀
computed是“缓存计算器”,依赖不变不重算;methods是“手动工具”,每次调用都重算;watch是“监控雷达”,数据一变就触发动作。
Getter 必须纯,Setter 别自循环。
现在去你的项目里,找一个数组(比如userList),写一个computed来过滤出age > 18的用户列表,并尝试在模板中渲染。如果遇到 TS 类型报错,记得加上泛型computed<{...}[]>(() => ...)😄
