人人都会AI编程

计算属性:computed 基础与高级用法

更新时间:2026-07-10

计算属性(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
}

区别在于:方法每次调用都会重新执行,而计算属性会缓存结果。当 firstNamelastName 都没变时,多次访问 fullName.value 只会返回上一次的计算结果,函数体不会重新执行。这在依赖很多、计算开销较大(例如遍历大数组、过滤排序)时能避免不必要的性能浪费。

实用建议:只要是从已有数据派生出的值,优先考虑 computed 而不是在模板里写逻辑或调用方法。

高级用法一:可写的计算属性(提供 getter 和 setter)

通常计算属性是只读的,但你也可以定义一个带 set 的版本,用于反向解析。例如:根据 fullName 反向拆解出 firstNamelastName

<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 又依赖 pricequantity。任何一个基础值变化,整个链上的相关计算属性都会自动重新求值,且每个节点都保持自己独立的缓存。

高级用法三:在 computed 内部使用其他工具(如 watchref 结合)

虽然不常见,但可以在 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,代码会更清晰、更高效。