前言
在前几篇博客中,我们详细聊了 Vue 的响应式 API(ref/reactive)和侦听器(watch)。今天,我们把目光投向 Vue 中另一个核心概念------计算属性(computed)。
如果说 watch 是"当数据变化时做点什么",那么 computed 就是"根据已有数据计算出新数据 "。它们是 Vue 响应式系统中分工明确的两个工具:一个负责派生数据 ,一个负责执行副作用。
本文将系统梳理 Vue 2 和 Vue 3 中 computed 的用法差异、底层原理、实战案例和最佳实践,并通过与 methods 和 watch 的深度对比,帮你彻底掌握什么时候该用谁。
一、什么是 computed?为什么需要它?
1.1 从模板中的表达式说起
在 Vue 模板中,我们可以直接使用 JavaScript 表达式:
vue
<template>
<p>{{ message.split('').reverse().join('') }}</p>
</template>
这种写法虽然方便,但当逻辑变得复杂时,模板会变得臃肿且难以维护 。更重要的是,如果多个地方都需要同样的计算结果,代码就会重复。
1.2 computed 的核心价值
计算属性(computed)就是为解决这两个问题而生的:
- 逻辑复用:将复杂的计算逻辑封装成一个属性,在模板中多次使用
- 缓存机制 :基于依赖进行缓存,只有依赖变化时才重新计算
- 声明式编程:描述"数据应该如何计算",而非"什么时候计算"
一句话总结 :
computed用于根据已有的响应式数据派生出新的数据 ,并且这个派生结果会被缓存,直到依赖发生变化。
二、Vue 2 中的 computed(Options API)
2.1 基本用法:只读计算属性
在 Vue 2 中,computed 是一个选项对象 ,与 data、methods 平级。计算属性的值是一个函数,函数的返回值就是计算属性的值。
javascript
export default {
data() {
return {
firstName: '张',
lastName: '三',
price: 100,
quantity: 3
}
},
computed: {
// 基础用法:拼接全名
fullName() {
return this.firstName + ' ' + this.lastName
},
// 复杂计算:商品总价
totalPrice() {
return this.price * this.quantity
},
// 带条件判断的计算
discountPrice() {
const total = this.price * this.quantity
return total > 200 ? total * 0.9 : total
}
}
}
在模板中使用时,不需要加括号,就像使用普通属性一样:
vue
<template>
<p>全名:{{ fullName }}</p>
<p>总价:{{ totalPrice }}</p>
<p>折后价:{{ discountPrice }}</p>
</template>
2.2 可写计算属性:提供 setter
计算属性默认是只读的------你只能获取它的值,不能修改它。但在某些场景下(如表单的双向绑定),你可能需要一个"可写"的计算属性。
javascript
export default {
data() {
return {
firstName: '张',
lastName: '三'
}
},
computed: {
fullName: {
// getter:读取时触发
get() {
return this.firstName + ' ' + this.lastName
},
// setter:赋值时触发
set(newVal) {
const names = newVal.split(' ')
this.firstName = names[0]
this.lastName = names[names.length - 1]
}
}
}
}
vue
<template>
<!-- 修改 fullName 会自动拆分到 firstName 和 lastName -->
<input v-model="fullName" placeholder="输入全名" />
<p>姓:{{ firstName }},名:{{ lastName }}</p>
</template>
2.3 computed vs data:不要重复定义
计算属性不能与 data 中的属性重名 ,否则会造成冲突。计算属性的值由函数计算得出,不需要在 data 中预先定义。
2.4 底层原理:基于 Watcher 的缓存机制
Vue 2 的计算属性底层依赖于 Watcher 机制:
- 初始化 :Vue 在
initState阶段为每个计算属性创建一个计算属性 Watcher - 依赖收集 :首次访问计算属性时,将当前 Watcher 设置为
Dep.target,执行 getter 函数时收集依赖 - 缓存机制 :每个计算属性 Watcher 都有一个
dirty标志dirty = true:需要重新计算dirty = false:直接返回缓存值
- 触发更新 :当依赖的响应式数据变化时,通知计算属性 Watcher,将
dirty设为true - 重新计算 :下次访问计算属性时,发现
dirty = true,重新执行 getter 并更新缓存
这种设计保证了:只要依赖没有变化,多次访问计算属性都会立即返回缓存值,不会重复执行计算逻辑。
三、Vue 3 中的 computed(组合式 API)
3.1 基本用法
在 Vue 3 中,computed 是一个可导入的函数 ,在 setup() 或 <script setup> 中使用。
使用 computed() 函数:
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const price = ref(100)
const quantity = ref(3)
// 只读计算属性:传入一个 getter 函数
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
const totalPrice = computed(() => {
return price.value * quantity.value
})
// 复杂的计算逻辑
const discountPrice = computed(() => {
const total = price.value * quantity.value
return total > 200 ? total * 0.9 : total
})
</script>
vue
<template>
<!-- 模板中自动解包,不需要 .value -->
<p>全名:{{ fullName }}</p>
<p>总价:{{ totalPrice }}</p>
</template>
注意 :在
<script>中访问computed的值需要.value(因为computed()返回的是一个 ref 对象),但在模板中会自动解包。
3.2 可写计算属性:传入对象
与 Vue 2 类似,Vue 3 也支持可写的计算属性:
vue
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 可写计算属性:传入包含 get 和 set 的对象
const fullName = computed({
// getter
get() {
return firstName.value + ' ' + lastName.value
},
// setter
set(newVal) {
const names = newVal.split(' ')
firstName.value = names[0]
lastName.value = names[names.length - 1]
}
})
</script>
3.3 Vue 2 与 Vue 3 的语法对比
| 对比维度 | Vue 2(Options API) | Vue 3(组合式 API) |
|---|---|---|
| 定义方式 | computed 选项对象 |
computed() 函数 |
| 导入 | 无需导入,直接使用 | 需要 import { computed } from 'vue' |
| 返回值 | 直接作为 this 的属性访问 |
返回一个 ref 对象 ,需 .value 访问 |
| 模板访问 | 直接使用 {``{ fullName }} |
自动解包,直接使用 {``{ fullName }} |
| 可写计算属性 | { get() {}, set() {} } |
{ get() {}, set() {} }(语法相同) |
3.4 底层原理:基于 Proxy 和 Effect 的懒更新
Vue 3 的计算属性基于 Proxy 和 Effect 机制实现:
核心数据结构:
javascript
// 简化版 ComputedRefImpl
class ComputedRefImpl {
constructor(getter) {
this._dirty = true // 脏标记:是否需要重新计算
this._value = undefined // 缓存的值
// 创建 effect,scheduler 在依赖变化时触发
this.effect = new ReactiveEffect(getter, () => {
this._dirty = true // 依赖变化时标记为"脏"
})
}
get value() {
if (this._dirty) {
this._value = this.effect.run() // 仅在脏时重新计算
this._dirty = false
}
return this._value
}
}
关键机制------懒更新(Lazy Evaluation) :
Vue 3 的 computed 采用懒更新 策略------只在计算属性被访问时才重新计算,而不是依赖一变化就立即计算。
这种设计的优势在于:
- 如果计算属性在某个渲染周期中没有被使用,即使依赖变化了也不会浪费性能去计算
- 多个依赖变化时,只会在最后一次被访问时计算一次,避免了中间状态的多次计算
脏标记(_dirty)机制 :
- 初始状态:
_dirty = true,首次访问时计算 - 依赖变化:触发 scheduler,设置
_dirty = true - 再次访问:检查
_dirty,为true时重新计算并缓存
四、真实项目实战
4.1 场景一:购物车计算
这是 computed 最经典的实战场景------计算购物车的商品数量、总价、优惠等。
Vue 3 实现:
vue
<template>
<div class="shopping-cart">
<h2>购物车</h2>
<!-- 商品列表 -->
<div v-for="item in cartItems" :key="item.id" class="cart-item">
<span>{{ item.name }}</span>
<button @click="decreaseQuantity(item)">-</button>
<span>{{ item.quantity }}</span>
<button @click="increaseQuantity(item)">+</button>
<span>¥{{ item.price * item.quantity }}</span>
<button @click="removeItem(item.id)">删除</button>
</div>
<!-- 汇总信息(全部由 computed 计算) -->
<div class="summary">
<p>商品总数:{{ totalQuantity }} 件</p>
<p>商品总价:¥{{ totalPrice }}</p>
<p v-if="discount > 0">优惠:-¥{{ discount }}</p>
<p>实付金额:<strong>¥{{ finalPrice }}</strong></p>
<p v-if="isFreeShipping">🎉 已满足免运费条件</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// ====== 原始数据 ======
const cartItems = ref([
{ id: 1, name: 'Vue 实战指南', price: 89, quantity: 1 },
{ id: 2, name: '机械键盘', price: 299, quantity: 1 },
{ id: 3, name: '蓝牙耳机', price: 199, quantity: 0 }
])
// ====== 操作方法 ======
const increaseQuantity = (item) => {
item.quantity++
}
const decreaseQuantity = (item) => {
if (item.quantity > 1) {
item.quantity--
}
}
const removeItem = (id) => {
cartItems.value = cartItems.value.filter(item => item.id !== id)
}
// ====== 计算属性 ======
// 1. 筛选有效商品(数量 > 0)
const validItems = computed(() => {
return cartItems.value.filter(item => item.quantity > 0)
})
// 2. 商品总数
const totalQuantity = computed(() => {
return validItems.value.reduce((sum, item) => sum + item.quantity, 0)
})
// 3. 商品总价
const totalPrice = computed(() => {
return validItems.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
// 4. 优惠金额(满300减30)
const discount = computed(() => {
if (totalPrice.value >= 300) return 30
if (totalPrice.value >= 200) return 15
return 0
})
// 5. 最终价格
const finalPrice = computed(() => {
return totalPrice.value - discount.value
})
// 6. 是否免运费(满200免运费)
const isFreeShipping = computed(() => {
return totalPrice.value >= 200
})
</script>
为什么用 computed 而不是 methods? 购物车的汇总信息会在用户增删商品时频繁变化,使用
computed可以确保只有依赖变化时才重新计算 ,而且多个汇总属性之间可以相互依赖 (如discount依赖totalPrice,finalPrice依赖totalPrice和discount),这是methods难以优雅实现的。
4.2 场景二:表单联动计算
在实际项目中,表单字段之间经常存在联动关系------选择一个值,自动计算另一个值。
vue
<template>
<div class="order-form">
<h2>订单计算</h2>
<div>
<label>商品单价:</label>
<input type="number" v-model="unitPrice" />
</div>
<div>
<label>购买数量:</label>
<input type="number" v-model="quantity" />
</div>
<div>
<label>折扣率(%):</label>
<input type="number" v-model="discountRate" min="0" max="100" />
</div>
<!-- 自动计算的结果 -->
<div class="result">
<p>小计:¥{{ subtotal }}</p>
<p>折扣金额:-¥{{ discountAmount }}</p>
<p>应付金额:<strong>¥{{ finalAmount }}</strong></p>
<p v-if="isHighValue">💰 大额订单,已为您升级 VIP 服务</p>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const unitPrice = ref(100)
const quantity = ref(2)
const discountRate = ref(10) // 10%
// 小计
const subtotal = computed(() => {
return unitPrice.value * quantity.value
})
// 折扣金额
const discountAmount = computed(() => {
return (subtotal.value * discountRate.value) / 100
})
// 最终金额
const finalAmount = computed(() => {
return subtotal.value - discountAmount.value
})
// 是否为大额订单(> 500)
const isHighValue = computed(() => {
return finalAmount.value > 500
})
</script>
4.3 场景三:待办事项的统计
待办事项列表是计算属性的经典应用场景------根据原始列表派生出各种统计数据。
vue
<template>
<div class="todo-app">
<h2>我的待办</h2>
<input v-model="newTodo" @keyup.enter="addTodo" placeholder="添加新待办" />
<ul>
<li v-for="todo in filteredTodos" :key="todo.id">
<input type="checkbox" v-model="todo.done" />
<span :class="{ done: todo.done }">{{ todo.text }}</span>
<button @click="deleteTodo(todo.id)">删除</button>
</li>
</ul>
<!-- 统计信息:全部由 computed 计算 -->
<div class="stats">
<span>全部:{{ total }}</span>
<span>已完成:{{ completed }}</span>
<span>未完成:{{ pending }}</span>
<span>完成率:{{ completionRate }}%</span>
</div>
<!-- 筛选按钮 -->
<div class="filters">
<button @click="filter = 'all'" :class="{ active: filter === 'all' }">全部</button>
<button @click="filter = 'active'" :class="{ active: filter === 'active' }">未完成</button>
<button @click="filter = 'completed'" :class="{ active: filter === 'completed' }">已完成</button>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// ====== 数据 ======
const todos = ref([
{ id: 1, text: '学习 Vue 3 计算属性', done: true },
{ id: 2, text: '写一篇博客', done: false },
{ id: 3, text: '分享给朋友', done: false }
])
const newTodo = ref('')
const filter = ref('all')
let nextId = 4
// ====== 方法 ======
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: nextId++,
text: newTodo.value.trim(),
done: false
})
newTodo.value = ''
}
}
const deleteTodo = (id) => {
todos.value = todos.value.filter(t => t.id !== id)
}
// ====== 计算属性(核心) ======
// 1. 基础统计
const total = computed(() => todos.value.length)
const completed = computed(() => todos.value.filter(t => t.done).length)
const pending = computed(() => todos.value.filter(t => !t.done).length)
const completionRate = computed(() => {
if (total.value === 0) return 0
return Math.round((completed.value / total.value) * 100)
})
// 2. 筛选后的列表
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(t => !t.done)
case 'completed':
return todos.value.filter(t => t.done)
default:
return todos.value
}
})
// 3. 是否全部完成
const allDone = computed(() => {
return total.value > 0 && completed.value === total.value
})
</script>
五、computed vs methods vs watch:全面对比与选择指南
5.1 核心区别速览
| 对比维度 | computed | methods | watch |
|---|---|---|---|
| 核心定位 | 派生数据 | 执行操作 | 响应变化 |
| 缓存机制 | ✅ 有缓存 | ❌ 无缓存 | ❌ 无缓存 |
| 调用方式 | 自动(依赖变化时) | 手动调用 | 自动(监听数据变化时) |
| 返回值 | 必须 return | 可选 return | 不需要 return |
| 适用场景 | 数据计算、派生 | 事件处理、逻辑复用 | 异步操作、副作用 |
| 模板中使用 | 作为属性 {``{ fullName }} |
作为方法 {``{ format() }} |
不在模板中使用 |
5.2 computed vs methods
相同点:两者都可以在模板中调用并返回计算结果。
核心区别 :缓存
vue
<template>
<div>
<!-- computed:依赖没变,直接从缓存读取,不会重复执行 -->
<p>{{ computedValue }}</p>
<p>{{ computedValue }}</p>
<p>{{ computedValue }}</p>
<!-- methods:每次调用都会重新执行函数 -->
<p>{{ methodValue() }}</p>
<p>{{ methodValue() }}</p>
<p>{{ methodValue() }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
// computed:有缓存,只有 count 变化时才重新计算
const computedValue = computed(() => {
console.log('computed 执行了')
return count.value * 2
})
// methods:每次调用都执行
const methodValue = () => {
console.log('method 执行了')
return count.value * 2
}
</script>
选择建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 需要派生数据,且会被多次访问 | computed |
利用缓存,避免重复计算 |
| 需要传参进行计算 | methods |
computed 不支持传参 |
| 计算开销大,但依赖变化不频繁 | computed |
缓存能显著提升性能 |
| 需要主动触发的逻辑(如点击事件) | methods |
这是方法的天然职责 |
5.3 computed vs watch
相同点:两者都基于 Vue 的依赖追踪机制,都能响应数据变化。
核心区别:
| 对比维度 | computed | watch |
|---|---|---|
| 职责 | 计算并返回新值 | 执行副作用(异步、DOM 操作等) |
| 是否返回新数据 | ✅ 是 | ❌ 否 |
| 缓存 | ✅ 有 | ❌ 无 |
| 适用场景 | 一个数据受多个数据影响 | 一个数据影响多个数据 |
经典口诀:
- computed 擅长处理:一个数据受多个数据影响
- watch 擅长处理:一个数据影响多个数据
选择建议:
| 场景 | 推荐 | 原因 |
|---|---|---|
| 根据现有数据计算新数据 | computed |
这是 computed 的天然职责 |
| 数据变化时发起异步请求 | watch |
computed 不支持异步 |
| 数据变化时操作 DOM | watch |
computed 不应产生副作用 |
| 多个数据联动计算一个值 | computed |
依赖追踪自动管理 |
| 一个数据变化触发多个操作 | watch |
可以执行任意逻辑 |
5.4 综合决策流程图
需要根据数据变化做点什么?
│
├─ 需要计算出一个新值?
│ │
│ ├─ 是 ──→ 会被多次访问吗?
│ │ │
│ │ ├─ 是 ──→ 用 computed ✅(有缓存)
│ │ └─ 否 ──→ 用 computed 或 methods
│ │
│ └─ 否 ──→ 需要执行异步操作或复杂副作用?
│ │
│ ├─ 是 ──→ 用 watch ✅
│ └─ 否 ──→ 用 methods(主动触发)
│
└─ 是用户主动触发的操作(点击、输入等)?
│
└─ 是 ──→ 用 methods ✅
六、注意事项与最佳实践
6.1 ⚠️ computed 中不能使用异步操作
计算属性必须是同步的 ,不能包含异步操作(如 setTimeout、Promise、async/await):
javascript
// ❌ 错误:computed 不能是异步的
const asyncComputed = computed(async () => {
const res = await fetch('/api/data')
return res.json()
})
// ✅ 正确:异步操作应该放在 watch 或生命周期钩子中
const data = ref(null)
watch(someId, async (newId) => {
data.value = await fetchData(newId)
})
6.2 ⚠️ computed 中不要修改其他数据
计算属性应该只做计算,不产生副作用(不修改其他数据、不操作 DOM):
javascript
// ❌ 错误:computed 中产生了副作用
const total = computed(() => {
// 不应该在 computed 中修改其他数据
someOtherValue.value = price.value * 2
return price.value * quantity.value
})
// ✅ 正确:computed 只做纯计算
const total = computed(() => {
return price.value * quantity.value
})
6.3 ⚠️ 避免在 computed 中访问不存在的属性
在 computed 中访问 ref 时必须使用 .value,访问 reactive 对象时直接使用属性名:
javascript
const count = ref(0)
const state = reactive({ name: 'Vue' })
// ✅ 正确
const double = computed(() => count.value * 2)
const greeting = computed(() => `Hello, ${state.name}`)
// ❌ 错误:忘记 .value
const wrong = computed(() => count * 2) // count 是对象,不是数字
6.4 ⚠️ 计算属性的依赖必须是响应式的
computed 只能追踪响应式数据的变化:
javascript
// ❌ 错误:依赖的不是响应式数据
let plainNumber = 0
const badComputed = computed(() => plainNumber * 2)
// plainNumber 变化时,badComputed 不会更新
// ✅ 正确:依赖响应式数据
const reactiveNumber = ref(0)
const goodComputed = computed(() => reactiveNumber.value * 2)
6.5 ✅ 利用 computed 的链式依赖
计算属性可以相互依赖,Vue 会自动管理依赖链:
javascript
const price = ref(100)
const quantity = ref(3)
const subtotal = computed(() => price.value * quantity.value)
const tax = computed(() => subtotal.value * 0.1) // 依赖 subtotal
const total = computed(() => subtotal.value + tax.value) // 依赖 subtotal 和 tax
// 当 price 或 quantity 变化时,subtotal、tax、total 会自动按顺序更新
七、总结
Vue 2 vs Vue 3 computed 全景对比
| 对比维度 | Vue 2(Options API) | Vue 3(组合式 API) |
|---|---|---|
| 定义方式 | computed: { ... } 选项 |
computed() 函数 |
| 导入 | 无需导入 | import { computed } from 'vue' |
| 返回值类型 | 直接作为 this 属性 |
返回 ref 对象 |
| 模板访问 | {``{ fullName }} |
{``{ fullName }}(自动解包) |
| Script 访问 | this.fullName |
fullName.value |
| 可写计算属性 | { get() {}, set() {} } |
{ get() {}, set() {} } |
| 底层实现 | Object.defineProperty + Watcher |
Proxy + Effect |
| 缓存机制 | 基于 dirty 标志 |
基于 _dirty 标志 + 懒更新 |
核心要点回顾
- computed 是什么 :根据已有响应式数据派生新数据的工具
- 为什么要用 computed :缓存 机制避免重复计算,声明式编程让代码更清晰
- computed vs methods:computed 有缓存,methods 没有;computed 自动更新,methods 需要调用
- computed vs watch :computed 用于派生数据 ,watch 用于执行副作用;computed 是"计算",watch 是"响应"
- 选择原则 :需要计算新值用
computed,需要执行异步操作用watch,需要主动触发用methods
理解 computed 的演进和正确用法,能让你在处理数据派生、表单联动、列表统计等场景时更加得心应手。记住:能用 computed 的地方就用 computed,它会让你的代码更高效、更清晰、更易维护。