前言
在 Vue 应用中,计算属性 computed 是最常用也是最重要的特性之一。它让我们能够声明式地创建基于其他响应式数据的衍生状态。但很多开发者对 computed 的理解停留在表面,不知道它背后的缓存机制,也不清楚何时该用 computed、何时该用 methods。更有甚者,在 computed 中做大量复杂计算,导致性能问题而不自知。
本文将深入探讨 computed 的缓存哲学,通过原理分析和实战案例,帮我们掌握计算属性的正确使用姿势,避免重复计算,提升应用性能。
computed 的工作原理
懒计算:只在访问时求值
computed 的第一个重要特性是懒计算(Lazy Evaluation)。这意味着计算属性不会在创建时立即执行,而是在第一次读取它的值时才会进行计算:
import { ref, computed } from 'vue'
const count = ref(1)
const double = computed(() => {
console.log('double 被计算了')
return count.value * 2
})
// 第一次访问 double,触发计算
console.log(double.value) // 输出: "double 被计算了", 2
// 再次访问,使用缓存,不重新计算
console.log(double.value) // 只输出 2,没有计算日志
缓存机制:依赖不变就不重新计算
computed 最核心的特性是缓存。它会记录上一次计算的结果,只有当依赖的响应式数据发生变化时,才会重新计算。如同上述例子一样,当 count 的值没有变化时,重复访问 double,读取的是缓存中的值,并不会重新走计算流程。
依赖追踪:自动收集响应式依赖
computed 本质上是一个特殊的 effect,能够精确知道自己的依赖项,在计算属性执行时,访问到的响应式数据会被自动记录为依赖:
const a = ref(1)
const b = ref(2)
const c = ref(3)
const condition = ref(true)
const result = computed(() => {
console.log('result 重新计算')
// 只有 condition 为 true 时才会访问 a
// 为 false 时访问 b
if (condition.value) {
return a.value + c.value
} else {
return b.value + c.value
}
})
console.log(result.value) // 计算一次,依赖: condition, a, c
// 修改 b - 不会触发重新计算,因为当前依赖中不包含 b
b.value = 10
console.log(result.value) // 使用缓存
// 修改 condition
condition.value = false
console.log(result.value) // 重新计算,现在依赖变为 condition, b, c
// 现在修改 b 会触发重新计算
b.value = 20
console.log(result.value) // 重新计算
computed vs methods:性能对比
多次渲染时的表现差异
在开发中,我们可以使用 computed, 也可以使用 methods 来获取衍生数据。它们在功能上没有太大的区别,但在表现上缺有着本质上的区别:
<template>
<div>
<!-- 三次使用 computed -->
<p>Computed: {{ double }}</p>
<p>Computed: {{ double }}</p>
<p>Computed: {{ double }}</p>
<!-- 三次调用 methods -->
<p>Methods: {{ getDouble() }}</p>
<p>Methods: {{ getDouble() }}</p>
<p>Methods: {{ getDouble() }}</p>
<button @click="count++">增加</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
// computed:只会计算一次,缓存三次使用
const double = computed(() => {
console.log('computed 计算')
return count.value * 2
})
// methods:每次调用都执行
function getDouble() {
console.log('methods 执行')
return count.value * 2
}
</script>
性能对比实验
我们可以写一个简单的例子,对比两者的性能:
<template>
<div>
<p>渲染次数: {{ renderCount }}</p>
<p>Computed 结果: {{ expensiveComputed }}</p>
<p>Methods 结果: {{ expensiveMethod() }}</p>
<button @click="count++">更新 count</button>
<button @click="forceUpdate++">强制更新</button>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const forceUpdate = ref(0)
const renderCount = ref(0)
// 模拟耗时计算
function expensiveOperation() {
let result = 0
for (let i = 0; i < 1000000; i++) {
result += i
}
return result + count.value
}
// computed 版本
const expensiveComputed = computed(() => {
console.log('耗时计算开始 (computed)')
const start = performance.now()
const result = expensiveOperation()
const end = performance.now()
console.log(`耗时计算结束,用时: ${(end - start).toFixed(2)}ms`)
return result
})
// methods 版本
function expensiveMethod() {
console.log('耗时计算开始 (methods)')
const start = performance.now()
const result = expensiveOperation()
const end = performance.now()
console.log(`耗时计算结束,用时: ${(end - start).toFixed(2)}ms`)
return result
}
// 模拟重新渲染
watch(forceUpdate, () => {
renderCount.value++
})
</script>
上述代码中:
- 点击"更新 count"(依赖变化):
computed:重新计算一次methods:重新计算一次- 此时两者的耗时基本一致,没有太大的差别
- 点击"强制更新"(依赖未变化):
computed:使用缓存,不计算methods:不管依赖变不变,每次渲染都重新计算!- 这时两者的差别就体现出来了,
computed缓存的性能更好
何时用 computed,何时用 methods
基于以上对比,我们可以得出清晰的选择原则:
- 基于现有数据衍生出新值:用
computed - 事件处理、非响应式计算、需要传参等:用
methods
选择决策树
计算属性的性能陷阱
计算量过大:在 computed 中做复杂计算
computed 虽然会缓存结果,但如果计算本身非常耗时,第一次访问时还是会造成卡顿,因此我们并不推荐在 computed 中做大量复杂的计算:
// ❌ 不好的做法:在 computed 中做大数据处理
const processedData = computed(() => {
// 假设 data 是一个包含 10 万条记录的数组
return data.value
.filter(item => item.active)
.sort((a, b) => b.value - a.value)
.map(item => ({
id: item.id,
displayName: `${item.name} - ${item.category}`,
score: item.score * item.weight
}))
.reduce((acc, item) => {
// 复杂的聚合计算
if (!acc[item.category]) {
acc[item.category] = []
}
acc[item.category].push(item)
return acc
}, {})
})
这样当 data 变化时,computed 会重新执行整个复杂计算,可能导致界面卡顿。这种情况,我们一般推荐用多个 computed 去处理,而不是写在一个 computed 中:
const activeItems = computed(() =>
data.value.filter(item => item.active)
)
const sortedItems = computed(() =>
[...activeItems.value].sort((a, b) => b.value - a.value)
)
const formattedItems = computed(() =>
sortedItems.value.map(item => ({
id: item.id,
displayName: `${item.name} - ${item.category}`,
score: item.score * item.weight
}))
)
const groupedItems = computed(() =>
formattedItems.value.reduce((acc, item) => {
if (!acc[item.category]) {
acc[item.category] = []
}
acc[item.category].push(item)
return acc
}, {})
)
依赖过多:依赖太细导致频繁重新计算
当 computed 依赖了太多响应式数据时,任何一个小变化都会导致重新计算:
// ❌ 不好的做法:依赖太多,频繁重新计算
const userProfile = computed(() => {
return {
fullName: `${user.value.firstName} ${user.value.lastName}`,
age: user.value.age,
email: user.value.email,
phone: user.value.phone,
address: `${user.value.city} ${user.value.street}`,
permissions: user.value.roles.map(r => r.permissions).flat(),
lastLogin: formatDate(user.value.lastLogin),
// ... 更多依赖
}
})
如此一来,computed 几乎每次都会重新计算,丢失了缓存优势。这种情况,也是推荐用多个 computed 去处理:
const basicInfo = computed(() => ({
fullName: `${user.value.firstName} ${user.value.lastName}`,
age: user.value.age,
email: user.value.email
}))
const contactInfo = computed(() => ({
phone: user.value.phone,
address: `${user.value.city} ${user.value.street}`
}))
const permissionInfo = computed(() => ({
roles: user.value.roles,
permissions: user.value.roles.map(r => r.permissions).flat()
}))
const lastLoginInfo = computed(() => ({
lastLogin: formatDate(user.value.lastLogin)
}))
副作用问题:computed 中修改数据
computed 中,通常是禁止修改数据的,但缺经常有人这么做,这其实是一个严重的反模式:
// ❌ 绝对禁止:在 computed 中修改数据
const doubleCount = computed(() => {
count.value++ // 副作用!修改其他响应式数据
return count.value * 2
})
// ❌ 同样禁止:在 computed 中调用可能修改数据的函数
const userStatus = computed(() => {
if (!user.value) {
fetchUser() // 副作用!异步操作
return 'loading'
}
return user.value.status
})
正确做法其实是使用 watch 处理副作用:
watch(user, (newUser) => {
if (!newUser) {
fetchUser()
}
})
const userStatus = computed(() => {
return user.value?.status || 'loading'
})
为什么不能在 computed 中修改数据呢?
- 违反单向数据流:计算属性应该是纯函数,不应该有副作用
- 可能导致死循环:修改依赖 -> 触发重新计算 -> 再次修改 -> 无限循环
- 不可预测的行为:
computed的求值时机不确定,副作用会导致难以调试的问题
优化策略
拆分计算:一个复杂的 computed 拆成多个小的
这是最常用也最有效的优化策略。通过拆分,我们可以:
- 减少单个
computed的计算量 - 提高缓存命中率
- 让代码更容易理解
- 便于单元测试
缓存结果:对于极耗时的计算,使用 cache 模式
有些计算即使拆分后仍然很耗时,这时我们可以考虑手动缓存策略:
// 复杂的数据处理
import { shallowRef, computed } from 'vue'
// 方案1:使用 Map 缓存历史计算结果
const calculationCache = new Map()
const expensiveData = computed(() => {
const key = JSON.stringify({
data: rawData.value,
config: config.value
})
if (calculationCache.has(key)) {
console.log('使用缓存结果')
return calculationCache.get(key)
}
console.log('执行复杂计算')
const result = veryExpensiveCalculation(rawData.value, config.value)
calculationCache.set(key, result)
// 限制缓存大小
if (calculationCache.size > 100) {
const firstKey = calculationCache.keys().next().value
calculationCache.delete(firstKey)
}
return result
})
// 方案2:使用 LRU 缓存库(如 lru-cache)
import LRU from 'lru-cache'
const cache = new LRU({
max: 100, // 最多缓存100个结果
maxAge: 1000 * 60 * 5 // 缓存5分钟
})
const cachedComputation = computed(() => {
const key = generateKey(dep1.value, dep2.value)
if (cache.has(key)) {
return cache.get(key)
}
const result = expensiveComputation(dep1.value, dep2.value)
cache.set(key, result)
return result
})
使用 getter 和 setter:双向绑定时控制写操作
computed 默认只有 getter,但也可以提供 setter 来实现双向绑定:
const rawValue = ref(50)
const clampedValue = computed({
get() {
return rawValue.value
},
set(newValue) {
// 确保数值在 0-100 之间
rawValue.value = Math.max(0, Math.min(100, newValue))
}
})
性能优化总结
- 拆分大型
computed:将一个大计算拆分为多个小计算 - 避免在
computed中修改数据:保持纯函数 - 减少依赖粒度:只依赖真正需要的数据
- 使用缓存策略:对极耗时计算实现手动缓存
- 考虑使用
watch:需要副作用时用watch替代
使用原则
应该使用 computed 的场景:
- 从现有数据派生新数据
- 需要在模板中多次使用同一个表达式
- 计算逻辑较复杂,需要命名提高可读性
- 希望利用缓存避免重复计算
不应该使用 computed 的场景:
- 需要传参(用 methods)
- 每次都需要新值(如随机数、时间戳)
- 有副作用(修改其他数据)
- 异步操作(用 watch 或 methods)
代码审查要点
computed是否足够"纯"?(没有副作用)- 是否可以用
computed替代methods?(检查是否在模板中多次调用) computed的依赖是否都是响应式的?- 是否过度拆分?(拆分太多也会增加开销)
- 计算逻辑是否复杂到需要拆分为多个
computed?
结语
computed 的核心价值是缓存,而缓存的核心价值是避免不必要的重复计算。只有深刻理解这一点,才能真正用好 computed,写出高性能的 Vue 应用。
对于文章中错误的地方或有任何疑问,欢迎在评论区留言讨论!