Vue3 的 Computed(计算属性)核心定位仍是「基于响应式依赖的缓存式派生值计算」,但在组合式 API 加持下,用法更灵活、与响应式系统(ref/reactive)的配合更紧密,同时保留了 Vue2 中经实践验证的核心优势(依赖追踪+缓存)。以下从「核心原理→基础用法→高级场景→Pinia 集成→避坑指南」全面拆解 Vue3 专属特性。
一、Vue3 Computed 核心原理(与 Vue2 差异)
Vue3 基于 Proxy 实现响应式系统,Computed 的底层逻辑虽延续「依赖收集→缓存→触发更新」,但有两处关键优化:
- 依赖追踪更精准 :
Proxy能监听对象/数组的所有操作(如push/delete/索引修改),无需像 Vue2 那样对数组方法重写,Computed 对数组依赖的追踪更无死角; - 惰性求值增强:仅当 Computed 的「最终结果被访问」时才会触发首次计算(Vue2 部分场景存在提前计算的情况),未被使用的 Computed 即使依赖变化也不会执行,性能更优;
- 缓存机制不变 :仅依赖的响应式数据(
ref/reactive/Pinia 状态等)变化时,Computed 才会标记为「脏状态」,下次访问时重新计算并更新缓存。
二、基础用法:组合式 API 核心写法
Vue3 推荐使用 setup 语法糖(<script setup>),Computed 需通过 import { computed } from 'vue' 显式引入,支持「只读 Computed」和「可写 Computed」两种核心形式,且返回值均为 ref 对象(模板中自动解包,脚本中需用 .value 访问)。
1. 只读 Computed(最常用)
接收一个「计算函数」,返回只读的 ref 对象,适用于「基于现有响应式数据派生值」(如过滤、统计、格式转换)。
vue
<script setup>
import { ref, computed } from 'vue'
// 1. 基础用法:依赖 ref 数据
const count = ref(1)
const doubleCount = computed(() => {
console.log('仅依赖变化时执行') // 首次访问/count变化时触发,否则复用缓存
return count.value * 2
})
// 2. 依赖 reactive 数据
const user = reactive({
firstName: '李',
lastName: '四'
})
const fullName = computed(() => `${user.firstName}${user.lastName}`)
// 3. 嵌套 Computed:依赖其他 Computed
const formattedName = computed(() => `用户姓名:${fullName.value}`)
// 脚本中访问需加 .value(模板中直接用 {{ doubleCount }} 即可)
console.log(doubleCount.value) // 2
count.value = 2
console.log(doubleCount.value) // 4(触发重新计算)
</script>
<template>
<div>计数:{{ count }}</div>
<div>计数翻倍:{{ doubleCount }}</div> <!-- 自动解包,无需 .value -->
<div>{{ formattedName }}</div> <!-- 输出:用户姓名:李四 -->
</template>
- 关键:计算函数内必须访问响应式数据的「响应式属性」 (如
count.value、user.firstName),否则无法被依赖追踪。
2. 可写 Computed(修改原始数据)
接收一个包含 get(读取逻辑)和 set(修改逻辑)的对象,适用于「通过计算属性反向修改原始响应式数据」(如表单联合输入、数据拆分)。
vue
<script setup>
import { ref, computed } from 'vue'
const width = ref(100)
const height = ref(200)
// 可写 Computed:面积(修改面积时反向计算宽高)
const area = computed({
// get:读取时执行(同只读 Computed 逻辑)
get() {
return width.value * height.value
},
// set:修改 area 时执行,接收新值
set(newArea) {
// 假设宽高比保持 1:2,反向修改原始数据
width.value = Math.sqrt(newArea / 2)
height.value = width.value * 2
}
})
// 修改 Computed(触发 set 方法)
area.value = 800
console.log(width.value) // 20,height.value:40(符合 1:2 比例)
</script>
- 场景:如「尺寸调整器」(面积→宽高)、「日期范围选择」(总天数→开始/结束日期)、「密码强度计算」(输入值→强度等级,反向修改提示文案)。
三、高级场景:Vue3 专属用法
1. 与 reactive 嵌套对象的配合
Vue3 的 Proxy 支持深层响应式,Computed 可直接依赖 reactive 嵌套对象的属性,无需额外处理:
vue
<script setup>
import { reactive, computed } from 'vue'
const goods = reactive([
{ id: 1, name: '笔记本', price: 5999, stock: 10 },
{ id: 2, name: '鼠标', price: 299, stock: 30 }
])
// 计算有库存的商品数量
const availableGoodsCount = computed(() => {
return goods.filter(item => item.stock > 0).length
})
// 计算商品总价(依赖嵌套属性 price/stock)
const totalStockValue = computed(() => {
return goods.reduce((sum, item) => sum + item.price * item.stock, 0)
})
// 修改嵌套属性,触发 Computed 更新
goods[0].stock = 5
console.log(availableGoodsCount.value) // 2(仍有库存)
console.log(totalStockValue.value) // 5999*5 + 299*30 = 40960
</script>
2. 结合防抖/节流(处理高频计算)
Computed 本身无防抖节流能力,但可与 lodash 的 debounce/throttle 结合,处理「输入搜索过滤」等高频触发场景(需注意:防抖逻辑需包裹计算函数,且需手动处理 ref 依赖):
vue
<script setup>
import { ref, computed } from 'vue'
import { debounce } from 'lodash-es'
const searchInput = ref('')
const goods = ref([/* 商品列表 */])
// 防抖处理:输入停止 300ms 后再执行过滤(避免输入时频繁计算)
const debouncedFilter = debounce((val) => {
return goods.value.filter(item => item.name.includes(val))
}, 300)
// Computed 依赖 searchInput,触发防抖过滤
const filteredGoods = computed(() => {
return debouncedFilter(searchInput.value)
})
</script>
<template>
<input v-model="searchInput" placeholder="搜索商品" />
<div v-for="item in filteredGoods" :key="item.id">{{ item.name }}</div>
</template>
- 注意:防抖函数需用
lodash-es(ES 模块版本),避免 CommonJS 模块导致的打包问题。
3. 依赖异步数据(配合 async/await)
Computed 本身不支持 async/await(计算函数需同步返回值),若需依赖异步数据(如接口请求结果),需先通过 ref 存储异步结果,再用 Computed 基于该 ref 计算:
vue
<script setup>
import { ref, computed, onMounted } from 'vue'
import axios from 'axios'
// 存储异步数据(响应式)
const userList = ref([])
// 异步请求数据
onMounted(async () => {
const res = await axios.get('/api/users')
userList.value = res.data
})
// Computed 依赖异步获取的 userList(数据更新后自动重新计算)
const activeUserCount = computed(() => {
return userList.value.filter(user => user.status === 'active').length
})
</script>
4. Pinia 中的 Computed(状态派生)
Pinia 是 Vue3 官方状态管理库,其 getters 本质就是 Computed,支持依赖 Pinia 状态、其他 getters,且自带缓存:
javascript
// stores/user.js
import { defineStore } from 'pinia'
export const useUserStore = defineStore('user', {
state: () => ({
users: [
{ id: 1, name: '张三', age: 20 },
{ id: 2, name: '李四', age: 25 }
]
}),
// getters = Pinia 中的 Computed
getters: {
// 1. 基础派生:统计成年用户
adultUsers: (state) => {
return state.users.filter(user => user.age >= 18)
},
// 2. 依赖其他 getters
adultUserCount: (state, getters) => {
return getters.adultUsers.length // 依赖 adultUsers
},
// 3. 接收参数(本质是返回函数的 Computed)
getUserById: (state) => {
// 返回函数,支持传参(但会失去缓存,每次调用都执行)
return (id) => state.users.find(user => user.id === id)
}
}
})
在组件中使用:
vue
<script setup>
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
console.log(userStore.adultUserCount) // 2
console.log(userStore.getUserById(1).name) // 张三
</script>
四、Vue3 Computed 避坑指南(重点!)
1. 忘记 .value 导致的错误(最常见)
Computed 返回的是 ref 对象,脚本中访问时必须加 .value,模板中会自动解包(无需加):
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
const double = computed(() => count.value * 2)
console.log(double) // ❌ 输出 RefImpl 对象(不是具体值)
console.log(double.value) // ✅ 输出 2
</script>
2. 依赖非响应式数据(无法触发更新)
Computed 仅能追踪 ref/reactive/Pinia 状态等「响应式数据」,非响应式数据(如普通变量、Math.random()、Date.now())的变化不会触发重新计算:
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
const nonReactive = 2 // 非响应式变量
const badComputed = computed(() => {
return count.value + nonReactive + Math.random()
})
count.value = 2 // 触发更新(count 是响应式)
nonReactive = 3 // 不触发更新(非响应式)
</script>
- 修正:非响应式数据需用
ref包装(const nonReactive = ref(2))。
3. 在 Computed 中修改响应式数据(无限循环)
Computed 的核心是「计算派生值」,而非「修改原始数据」,否则会导致依赖变化→Computed 重新执行→再次修改数据→无限循环:
vue
<script setup>
import { ref, computed } from 'vue'
const count = ref(1)
const badComputed = computed(() => {
count.value++ // ❌ 禁止在 Computed 中修改响应式数据
return count.value * 2
})
</script>
- 修正:修改数据应放在
watch、事件回调或methods中。
4. 复杂计算未拆分(可读性/性能差)
单个 Computed 函数包含多层逻辑(过滤+排序+格式转换)时,需拆分为多个简单 Computed,既提升可读性,又能利用缓存(中间结果复用):
vue
<script setup>
import { ref, computed } from 'vue'
const goods = ref([/* 商品列表 */])
// 优化前:复杂逻辑聚合
const badComputed = computed(() => {
return goods.value
.filter(item => item.price > 1000)
.sort((a, b) => b.price - a.price)
.map(item => `【${item.name}】¥${item.price}`)
})
// 优化后:拆分多个 Computed
const expensiveGoods = computed(() => goods.value.filter(item => item.price > 1000))
const sortedGoods = computed(() => [...expensiveGoods.value].sort((a, b) => b.price - a.price))
const formattedGoods = computed(() => sortedGoods.value.map(item => `【${item.name}】¥${item.price}`))
</script>
5. 误解「可写 Computed」的使用场景
可写 Computed 的 set 方法必须反向修改原始依赖数据,否则修改 Computed 后不会同步到原始数据,导致状态不一致:
vue
<script setup>
import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)
// 错误:set 方法未修改原始依赖
const sum = computed({
get() { return a.value + b.value },
set(newValue) {
console.log(newValue) // 仅打印,未修改 a/b
}
})
sum.value = 5 // a 和 b 仍为 1 和 2,sum 下次访问时会恢复为 3
</script>
// 正确:set 方法修改原始依赖
const sum = computed({
get() { return a.value + b.value },
set(newValue) {
a.value = newValue - b.value // 反向计算 a 的值
}
})
sum.value = 5 // a 变为 3,sum 保持 5(a=3 + b=2)
</script>