计算属性(computed)是 Vue 响应式系统中最常用的派生状态工具。它的核心价值在于:基于已有响应式数据,声明一个会随依赖自动更新、并且带有缓存的新值。在组合式 API 中,通过从 vue 导入的 computed 函数来创建。
基础用法:声明“依赖”与“结果”的关系
computed 接收一个返回值的函数,这个函数的执行依赖某些响应式数据,Vue 会自动追踪这些依赖。当依赖的数据变化时,计算属性重新求值;依赖不变时,直接返回上一次的缓存结果。
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 基础计算属性:拼接全名
const fullName = computed(() => {
return firstName.value + lastName.value
})
</script>
<template>
<p>{{ fullName }}</p> <!-- 显示“张三” -->
</template>
fullName 就是一个典型的计算属性。在模板中使用时,它就像一个普通的响应式数据,但是你不能手动修改它(除非显式提供 setter)。
缓存机制:为什么不用方法?
很多人会问:用普通函数也能返回拼接结果,为什么要用 computed?
function getFullName() {
return firstName.value + lastName.value
}
区别在于:方法每次调用都会重新执行,而计算属性会缓存结果。当 firstName 和 lastName 都没变时,多次访问 fullName.value 只会返回上一次的计算结果,函数体不会重新执行。这在依赖很多、计算开销较大(例如遍历大数组、过滤排序)时能避免不必要的性能浪费。
实用建议:只要是从已有数据派生出的值,优先考虑
computed而不是在模板里写逻辑或调用方法。
高级用法一:可写的计算属性(提供 getter 和 setter)
通常计算属性是只读的,但你也可以定义一个带 set 的版本,用于反向解析。例如:根据 fullName 反向拆解出 firstName 和 lastName。
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get() {
return firstName.value + lastName.value
},
set(newValue) {
// 简单拆分规则:第一个字是姓,其余是名
firstName.value = newValue[0] || ''
lastName.value = newValue.slice(1) || ''
}
})
</script>
<template>
<input v-model="fullName" />
<!-- 输入“李四”时,firstName 变为“李”,lastName 变为“四” -->
</template>
这在使用 v-model 绑定一个组合字段时特别有用,让计算属性既能“读”又能“写”。
高级用法二:依赖多个数据源的复杂计算
计算属性内可以访问其他计算属性,形成依赖链,Vue 会自动梳理正确的求值顺序。
const price = ref(100)
const quantity = ref(2)
const discount = ref(0.8)
const subtotal = computed(() => price.value * quantity.value)
const total = computed(() => subtotal.value * discount.value)
这里 total 依赖 subtotal,而 subtotal 又依赖 price 和 quantity。任何一个基础值变化,整个链上的相关计算属性都会自动重新求值,且每个节点都保持自己独立的缓存。
高级用法三:在 computed 内部使用其他工具(如 watch、ref 结合)
虽然不常见,但可以在 computed 内部使用其他组合式函数,比如动态创建一个临时的 ref 来进行中间计算。不过要谨慎,避免引入副作用,否则会破坏计算属性的“纯计算”特性。
真实场景示例:过滤与排序列表
假设你有一个商品列表,需要在前端做搜索过滤和排序,这种场景非常适合用计算属性。
<script setup>
import { ref, computed } from 'vue'
const searchQuery = ref('')
const sortBy = ref('price') // 'price' 或 'name'
const products = ref([
{ id: 1, name: '键盘', price: 299 },
{ id: 2, name: '鼠标', price: 129 },
{ id: 3, name: '显示器', price: 1899 }
])
// 过滤后的产品列表
const filteredProducts = computed(() => {
return products.value.filter(p => p.name.includes(searchQuery.value))
})
// 最终展示的列表:过滤 + 排序
const displayProducts = computed(() => {
const list = filteredProducts.value
return [...list].sort((a, b) => {
if (sortBy.value === 'price') return a.price - b.price
return a.name.localeCompare(b.name)
})
})
</script>
<template>
<input v-model="searchQuery" placeholder="搜索商品" />
<select v-model="sortBy">
<option value="price">按价格排序</option>
<option value="name">按名称排序</option>
</select>
<ul>
<li v-for="p in displayProducts" :key="p.id">{{ p.name }} - ¥{{ p.price }}</li>
</ul>
</template>
任何输入或排序条件变化,displayProducts 都会自动重新计算并提供新列表。而且由于缓存,即便频繁切换排序方式,只要原始数据没变,过滤步骤不会重复执行。
注意事项与常见坑
- 不要修改计算属性依赖的数据:计算属性的 getter 应保持“副作用纯净”,不要在内部直接修改
ref或调用 API,否则可能导致无限循环更新。 - 计算属性不是懒加载的默认:一旦模板中使用了计算属性,它会立刻求值。如果你需要“访问时才计算”且依赖可能没准备好的情况,可以改用
watchEffect或手动控制。 - 避免返回新对象:如果计算返回一个新对象或数组(如
computed(() => ({ ...obj }))),每次依赖变化都会生成新引用,可能导致子组件不必要的重渲染。如果不需要总是新引用,可直接返回原始引用或使用shallowRef+手动控制。
总结:何时用 computed?
- 你需要从现有
ref/reactive衍生出一个新值,并在多个地方使用。 - 这个衍生值的计算成本较高,希望自动缓存。
- 你希望保持模板简洁,不包含复杂逻辑。
记住这句话:computed 是“声明了一个东西如何由其他东西算出来”,而不是“去执行一个动作”。把业务中的数据依赖关系交给 computed,代码会更清晰、更高效。