计算属性是 Vue 中使用频率最高的工具之一。它解决了一个朴素的问题:“某个值需要依赖其他数据计算得出,且计算过程可能较重,我不希望每次用都重新算一遍。” 它的底层实现依赖两个关键行为:缓存和懒执行。
基础用法回顾
<template>
<div>
<p>原价:{{ price }} 元</p>
<p>折后价:{{ discountedPrice }} 元</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const price = ref(100)
const discount = ref(0.8)
const discountedPrice = computed(() => {
console.log('正在计算折后价')
return (price.value * discount.value).toFixed(2)
})
</script>
在这个例子中,discountedPrice 只在 price 或 discount 变化时才会重新计算。模板中多次使用 discountedPrice,也只会执行一次计算函数。
缓存机制:依赖不变,直接返回上次结果
计算属性内部会维护一个“脏检查”标记。当它被读取时:
- 检查自己的依赖项(
price、discount)是否有变化。 - 如果依赖项都没变,直接返回上次缓存的结果,跳过函数执行。
- 如果有任何一个依赖项发生了改变,重新执行计算函数,更新缓存值,并告诉 Vue “我现在干净了”。
这避免了在模板或方法中反复调用计算函数的性能浪费。尤其是当计算逻辑包含数组遍历、正则匹配或复杂聚合运算时,缓存的意义非常明显。
与方法的对比:
<template>
<p>{{ getDiscountPrice() }}</p>
<p>{{ getDiscountPrice() }}</p>
<p>{{ getDiscountPrice() }}</p>
</template>
<script setup>
function getDiscountPrice() {
return (price.value * discount.value).toFixed(2)
}
</script>
如果使用普通方法,每次模板重新渲染(哪怕其他无关数据变化),三个 getDiscountPrice() 都会被执行三次。而计算属性 discountedPrice 在模板中引用三次,只会在首次访问或依赖变化时计算一次,其余两次直接取缓存。
懒执行:不访问就不计算,不浪费资源
计算属性的函数不会在创建时立即执行(这是与 watch 的一大区别)。它只在被读取时才判断是否需要计算。这就叫“懒执行”。
考虑一个比较消耗资源的计算属性,比如对大型列表做过滤和排序:
const filteredList = computed(() => {
return hugeList.value
.filter(item => item.status === 'active')
.sort((a, b) => b.score - a.score)
})
如果当前页面根本没有使用 filteredList(比如它只在某个不常打开的 Tab 中展示),那么即便 hugeList 频繁变化,计算属性的函数也不会被触发——因为根本没有代码去读取它。这就避免了无谓的计算。
只有当渲染函数或其他逻辑显式读取 filteredList.value 时,Vue 才会检查依赖,必要时执行计算函数。这种“按需计算”的设计让计算属性天然适合与条件渲染配合使用。
底层实现关键点(简化理解)
Vue 3 使用 computed 函数创建一个 ComputedRefImpl 实例,其核心逻辑可以概括为:
- 内部存储一个
_value缓存值和一个_dirty标记(初始为true)。 - 当读取
.value时:
- 如果
_dirty为true,运行 getter 函数,用返回结果更新_value,并将_dirty置为false。在执行 getter 时,会通过响应式系统的track收集依赖。 - 如果
_dirty为false,直接返回_value。
- 当任何一个被收集的依赖项发生改变时,触发 scheduler(不是立即执行计算),仅仅将
_dirty重新标记为true。等到下次.value被访问时,由于_dirty为true,才会重新计算。
这也就是为什么计算属性可以既响应变化,又不会在依赖变化时立刻执行。它只是在依赖变化时“标记脏”,把真正的计算延迟到下一次读取。
什么时候该用计算属性?
- 需要基于现有数据派生新数据,且该派生逻辑在多处使用。
- 派生过程有一定计算开销,希望避免重复执行。
- 派生结果需要随着依赖变化而自动更新。
示例场景:购物车总价、列表过滤与统计、表单字段的联动校验、任何“根据 A 和 B 得出 C”的逻辑。
注意事项
- 计算属性要作为纯函数使用,不要在 getter 中产生副作用(如修改其他状态、发送请求等)。副作用应放在
watch或方法中。 - 计算属性默认是只读的,但也可以提供
set方法来实现可写计算属性,适用于量少的双向绑定场景。 - 依赖必须是响应式数据(
ref、reactive等),否则变化无法被追踪,缓存机制失效。
一句话总结:计算属性 = 自动缓存的计算结果 + 仅在所需时更新。它是处理派生状态最轻松、最高效的方式,使用成本极低,但性能收益显著。