Vue.js: 计算属性 computed
最后更新:2026-08-26
计算属性(computed)是 Vue 3 中基于响应式数据自动重算的特殊属性。它的核心价值是缓存——只有依赖的数据变化时才重新计算,否则直接返回上次的值。理解 computed 是写出高性能 Vue 应用的关键。
计算属性的核心是 3 个特性:自动追踪依赖 + 缓存结果 + 响应式。本课帮你掌握它的 5 种用法和 3 个陷阱。
1. 你将学到
computed()基本语法和返回值- computed vs methods 的关键区别(缓存)
- getter/setter 双向计算属性
- 计算属性链式调用(computed 套 computed)
- computed 在模板和方法中的不同用法
- 3 个常见陷阱(误用、循环依赖、性能问题)
2. 一个购物车总价的性能问题
(1) 痛点:每次点击都重新算 100 个商品
Alice 在电商后台实现了购物车总价计算:
JS
// ❌ 翻车版:用 methods
function cartTotal(items) {
console.log('Calculating...') // 每次调用都打印
return items.reduce((sum, item) => sum + item.price * item.quantity, 0)
}
HTML
<template>
<div>
<p>Total: ${{ cartTotal(items) }}</p>
<p>Tax: ${{ cartTotal(items) * 0.08 }}</p>
<p>Grand Total: ${{ cartTotal(items) * 1.08 }}</p>
<button @click="showDate = new Date()">Update Time</button>
</div>
</template>
性能极差:
- 页面加载:计算运行 1 次 ✅
- 渲染一次:3 次(Total、Tax、Grand Total)= 300 次商品计算
- 用户点击"Update Time"(与商品无关):0 个变化,但计算仍然运行 3 次 = 300 次商品计算
性能监控工具显示:
"cartTotal 函数每次页面加载调用 5,000 次。100 个商品 × 5,000 = 500,000 次逐项计算。3 秒卡顿。"
(2) Vue computed 解法:缓存 + 依赖追踪
JS
import { ref, computed } from 'vue'
const items = ref([
{ price: 100, quantity: 2 },
{ price: 50, quantity: 1 }
])
// ✅ computed:只依赖 items,items 不变就不重算
const cartTotal = computed(() => {
console.log('Calculating...') // 只在 items 变化时打印
return items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
// computed 自动处理 3 处使用:只算 1 次
const tax = computed(() => cartTotal.value * 0.08)
const grandTotal = computed(() => cartTotal.value * 1.08)
(3) 收益
切换到 computed 后:
- 函数调用:5,000 → 100(-98%)
- 可交互时间:3s → 80ms(-97%)
- 滚动卡顿:5 次/秒 → 0(-100%)
- CPU 占用:80% → 5%(-94%)
3. computed() 基本用法
(1) 标准语法
JS
import { ref, computed } from 'vue'
const count = ref(0)
// 基础计算属性
const doubleCount = computed(() => {
return count.value * 2
})
// 简写
const doubleCount = computed(() => count.value * 2)
// 使用
console.log(doubleCount.value) // 0
count.value = 5
console.log(doubleCount.value) // 10
(2) 模板中使用
VUE
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="count++">+1</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
</script>
模板中 {{ doubleCount }} 不需要 .value(自动解包)。
(3) 5 大特性
graph TB
subgraph Computed核心特性
A[自动追踪依赖]
B[缓存结果]
C[响应式]
D[懒计算]
E[可链式调用]
end
style A fill:#42b883,color:#fff
style B fill:#42b883,color:#fff
style C fill:#42b883,color:#fff
style D fill:#42b883,color:#fff
style E fill:#42b883,color:#fff
| 特性 | 说明 |
|---|---|
| 自动追踪依赖 | 自动检测函数体中用到的响应式数据 |
| 缓存结果 | 依赖不变时多次访问只算 1 次 |
| 响应式 | 依赖变化时自动重新计算 |
| 懒计算 | 不被访问就不算(节省性能) |
| 可链式 | computed 可以依赖其他 computed |
4. computed vs methods 5 大区别
| 维度 | computed | methods |
|---|---|---|
| 缓存 | ✅ 依赖不变就不重算 | ❌ 每次调用都重算 |
| 响应式 | ✅ 自动追踪依赖 | ❌ 不追踪(要手动传参) |
| 模板访问 | {{ doubleCount }} |
{{ doubleCount() }} |
| 参数 | ❌ 不支持 | ✅ 接受任意参数 |
| 适用场景 | 简单派生值 | 复杂逻辑、事件处理 |
(4) 5 个选择场景
| 场景 | 用 computed | 用 methods |
|---|---|---|
| 显示数字翻倍 | ✅ | ❌ |
| 计算购物车总价 | ✅ | ❌ |
| 处理按钮点击 | ❌ | ✅ |
| 发送 API 请求 | ❌ | ✅ |
| 格式化日期(无依赖) | ❌ | ✅ |
| 过滤列表(依赖 data) | ✅ | ❌ |
| 表单校验 | ❌ | ✅ |
5. getter/setter 双向计算属性
(1) 默认只有 getter
JS
const firstName = ref('Alice')
const lastName = ref('Smith')
// 只有一个 getter
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
console.log(fullName.value) // Alice Smith
// fullName.value = 'Bob Lee' // ❌ 警告:计算属性是只读的
(2) 完整 getter + setter
JS
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
// 读取:拼起来
get() {
return `${firstName.value} ${lastName.value}`
},
// 写入:拆开
set(newValue) {
const parts = newValue.split(' ')
firstName.value = parts[0]
lastName.value = parts[1] || ''
}
})
// ✅ 现在可读可写
fullName.value = 'Bob Lee'
console.log(firstName.value) // Bob
console.log(lastName.value) // Lee
(3) 模板中双向绑定
VUE
<template>
<div>
<input v-model="fullName">
<p>First: {{ firstName }}</p>
<p>Last: {{ lastName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const parts = val.split(' ')
firstName.value = parts[0]
lastName.value = parts[1] || ''
}
})
6. 计算属性链式调用
(1) 嵌套 computed
JS
const items = ref([
{ price: 100, quantity: 2 },
{ price: 50, quantity: 3 }
])
// 第一层:原价合计
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
// 第二层:税(基于 subtotal)
const tax = computed(() => subtotal.value * 0.08)
// 第三层:折扣(基于 subtotal)
const discount = computed(() => subtotal.value > 200 ? 20 : 0)
// 第四层:最终总价
const total = computed(() => subtotal.value + tax.value - discount.value)
console.log(total.value) // 350 * 1.08 - 20 = 358
(2) 链式 computed 缓存机制
graph TB
A[items change] --> B[subtotal 重新计算]
B --> C[tax 重新计算]
B --> D[discount 重新计算]
C --> E[total 重新计算]
D --> E
style A fill:#42b883
style B fill:#42b883
style E fill:#42b883
只有依赖链上的 computed 会重算,未被使用的不会算。
7. 完整示例:电商购物车总价
▶ 示例:基础 computed
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>Subtotal: ${{ subtotal }}</p>
<p>Tax (8%): ${{ tax }}</p>
<p>Discount: -${{ discount }}</p>
<p><strong>Total: ${{ total }}</strong></p>
<button @click="addItem">+ 加 1 件 Case</button>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const items = ref([
{ name: 'iPhone', price: 999, quantity: 1 },
{ name: 'Case', price: 50, quantity: 2 }
])
const subtotal = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const tax = computed(() => subtotal.value * 0.08)
const discount = computed(() => subtotal.value > 1000 ? 50 : 0)
const total = computed(() => subtotal.value + tax.value - discount.value)
function addItem() {
items.value.push({ name: 'Case', price: 50, quantity: 1 })
}
return { subtotal, tax, discount, total, addItem }
}
}).mount('#app')
</script>
▶ 示例:getter + setter
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<input v-model="fullName" placeholder="First Last">
<p>First: {{ firstName }}</p>
<p>Last: {{ lastName }}</p>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val) => {
const parts = val.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
}
})
return { firstName, lastName, fullName }
}
}).mount('#app')
</script>
▶ 示例:computed 链式调用
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>基础分: {{ baseScore }}</p>
<p>附加分: {{ difficultyBonus }}</p>
<p>总分: {{ baseScore + difficultyBonus }}</p>
<p>等级: {{ grade }}</p>
<button @click="score += 5">+5 分</button>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const score = ref(85)
// 第一层:基础分
const baseScore = computed(() => score.value)
// 第二层:附加分
const difficultyBonus = computed(() => {
if (baseScore.value >= 90) return 10
if (baseScore.value >= 80) return 5
return 0
})
// 第三层:等级
const grade = computed(() => {
const total = baseScore.value + difficultyBonus.value
if (total >= 95) return 'A+'
if (total >= 90) return 'A'
if (total >= 80) return 'B'
return 'C'
})
return { score, baseScore, difficultyBonus, grade }
}
}).mount('#app')
</script>
▶ 示例:computed vs methods 对比
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>Computed (1 次): {{ totalComputed }} | {{ totalComputed }} | {{ totalComputed }}</p>
<p>Methods (3 次): {{ getTotal() }} | {{ getTotal() }} | {{ getTotal() }}</p>
</div>
<script>
const { createApp, ref, computed } = Vue
createApp({
setup() {
const items = ref([
{ name: 'A', price: 100, quantity: 1 },
{ name: 'B', price: 50, quantity: 2 }
])
// ❌ methods:每次调用都重算
function getTotal() {
console.log('methods called') // 模板中多次使用会打印多次
return items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
}
// ✅ computed:只算 1 次
const totalComputed = computed(() => {
console.log('computed called') // 只打印 1 次
return items.value.reduce((sum, i) => sum + i.price * i.quantity, 0)
})
return { items, getTotal, totalComputed }
}
}).mount('#app')
</script>
▶ 示例:5 个性能对比场景
| 场景 | methods 调用次数 | computed 调用次数 |
|---|---|---|
| 模板中 1 处使用 | 1 | 1 |
| 模板中 3 处使用 | 3 | 1 |
| 模板中 10 处使用 | 10 | 1 |
| 父组件更新但 items 不变 | 重新调用 | 不重算 |
| 依赖变化 | 重新调用 | 重新计算 |
▶ 示例:5 个常见错误
| 错误 | 现象 | 解决 |
|---|---|---|
| 用 methods 替代 computed | 多次使用性能差 | 改用 computed |
| 误给 computed 传参 | 报错 | computed 不支持参数,用 methods |
| setter 写自己 | 无限循环 | setter 改依赖,不要 fullName.value = ... |
| 循环依赖 A→B→A | 死循环 | 拆掉其中一个依赖 |
| 复杂逻辑写在 computed | 难调试 | 拆出方法,computed 调方法 |
❓ 常见问题
Q computed 和 methods 到底用哪个?
A 能用 computed 就用 computed(缓存 + 自动追踪)。只有以下情况用 methods:(1) 需要传参;(2) 不依赖响应式数据;(3) 事件处理器(@click 等)。
Q computed 能传参吗?
A 不能。computed 只能有 0 个参数。如果需要传参,用 methods 替代:
getDouble(n) { return n * 2 },或用 computed 返回一个函数:computed(() => (n) => n * 2)。Q computed 在 setup 外能访问吗?
A 能。computed 返回的是 ref 对象,可以传给其他组件或函数。但要保持响应式,建议把 computed 当作"派生 ref"用。
Q computed 和 watch 有什么区别?
A computed 是"派生值"(基于数据计算新值),watch 是"监听器"(数据变化时执行副作用)。computed 适合纯计算,watch 适合发请求、设状态等。
Q computed 性能比 methods 好在哪?
A 缓存。methods 每次调用都执行函数,computed 只在依赖变化时执行。模板中 3 处用 computed,只算 1 次;用 methods 算 3 次。
Q setter 怎么写才不无限循环?
A setter 中只改 computed 的依赖(ref),不要赋值给 computed 自己。例如
fullName 的 setter 应该改 firstName / lastName,不是 fullName.value = ...。Q computed 可以异步吗?
A 不能。computed 是同步计算,异步用 watch 或 watchEffect。如果需要"异步计算属性",用
data() { return { result: null } } + watch + onMounted 组合。📖 小节
- computed 是基于响应式数据自动重算的"派生 ref",5 大特性:自动追踪、缓存、响应式、懒计算、可链式
- 模板中
{{ doubleCount }}不需要 .value(自动解包),JS 中doubleCount.value - computed vs methods:computed 缓存(性能优),methods 不缓存但能传参
- getter/setter 双向计算属性:可读可写,setter 中改依赖不写自己
- 计算属性链式调用:computed 可以依赖其他 computed,未被使用的不重算
- 3 个陷阱:循环依赖、误用 methods、复杂逻辑难调试
- 性能:模板中 3 处用 computed 只算 1 次,methods 算 3 次
📝 作业
-
基础题(难度⭐) 写一个计算属性
wordCount,统计一段文本的单词数(用 split 拆分空格)。数据:
const text = ref('Hello Vue 3 from Alice')应输出 4。 -
进阶题(难度⭐⭐) 写一个购物车总价计算,包含 3 个计算属性:
subtotal:所有商品 price × quantity 之和discount:满 100 减 10,满 200 减 30total:subtotal - discount- 3 个商品:iPhone $999 × 1, Case $50 × 2, Charger $30 × 1
-
挑战题(难度⭐⭐⭐) 实现一个完整的"购物车总价系统":
- 5 个商品,每个含 name/price/quantity
- subtotal / tax(8%) / discount(满 200 减 20) / total 4 个计算属性
- fullName 计算属性用 getter/setter,setter 中根据输入更新 firstName/lastName
- 测试 5 个计算属性的依赖追踪(修改任意商品后哪些会重算)
- 用 mermaid 画出计算属性依赖图