计算属性的缓存,指的是只要它依赖的响应式数据没变,无论你在模板里访问多少次,那个计算函数都只执行一次,之后全部直接返回上次算好的结果。依赖一变,缓存作废,下次访问时才会重新计算。这就是它和方法最本质的区别——方法每调用一次就重算一次,计算属性只在依赖变化时才重算。
很多人用了很久 Vue,只知道"计算属性有缓存",却说不清缓存什么时候生效、什么时候失效,结果在模板里放了一堆不会被追踪的东西,页面永远不更新也不知道为什么。今天这篇文章,编程狮就把 computed 的缓存机制一次讲透:先跑一个能直接观察缓存行为的最小示例,再拆 3 个真实使用要点,最后给你一张计算属性、方法、侦听器三者的选型清单。

一、计算属性的缓存到底是什么
1.1 一个能看见缓存的最小示例
下面这段代码可以直接放进任意 Vue 3 项目里跑。我们在计算函数里放一个打印语句,这样它到底执行了几次,控制台一清二楚:
import { ref, computed } from 'vue'
const price = ref(10)
const count = ref(2)
const total = computed(() => {
console.log('计算函数执行了')
return price.value * count.value
})
console.log(total.value) // 打印"计算函数执行了",返回 20
console.log(total.value) // 什么都不打印,直接返回 20
console.log(total.value) // 什么都不打印,直接返回 20
count.value = 5 // 改依赖,缓存作废
console.log(total.value) // 重新打印"计算函数执行了",返回 50
连着读三次 total.value,"计算函数执行了"只出现了一次。把 count 从 2 改成 5 之后再读,它才重新算。这就是缓存的全部秘密:以依赖为钥匙,依赖不变就复用结果。
Vue 在背后做的事可以粗略理解成"依赖追踪"。计算函数执行时,凡是读到的响应式数据(ref、reactive 里的字段)都会被记成这个计算属性的依赖;之后 Vue 监听这些依赖,一旦有变化就把缓存标记为脏。你不需要手动声明依赖是谁,Vue 会自动收集——这也是为什么计算属性写起来比手写缓存省事得多。
想把这套响应式基础打牢,可以顺着 Vue3 教程 的响应式章节系统过一遍。
1.2 常见误解
最大的误解是把"缓存"理解成"只算一次,永不更新"。实际上缓存是有条件的复用:依赖不变才复用,依赖一变立刻重算。所以计算属性既能省掉无谓的重复计算,又能保证结果始终和最新的响应式数据一致——这也是它比"手动把结果存进一个普通变量"更靠谱的原因,你不用操心什么时候该清缓存。
第二个常见误解是"计算属性可以传参数"。标准的 computed 不接受参数,它描述的是一个值,不是一个函数。真要按参数算,就写成返回函数的计算属性,或者干脆用 methods,详见第五节。
二、要点一:依赖不变就不重算,这是性能的来源
理解了上面那个示例,第一个要点就很自然了:把昂贵的计算放进 computed,能省掉大量重复劳动。
典型场景是列表过滤和统计。假设你有一个几百条的商品列表,要在页面上同时显示总数、总价、已选中的条目,如果这三处都写模板表达式或者方法调用,每次渲染都要把列表完整遍历三遍;而写成一个计算属性,三个地方读的是同一份缓存结果,列表没变就只遍历一次。
import { ref, computed } from 'vue'
const list = ref([
{ name: '键盘', price: 199, done: true },
{ name: '鼠标', price: 99, done: false },
{ name: '显示器', price: 899, done: true },
])
const doneList = computed(() => list.value.filter(i => i.done))
const total = computed(() => doneList.value.reduce((s, i) => s + i.price, 0))
这里还有一个容易被忽略的好处:计算属性之间可以互相依赖。total 依赖 doneList,doneList 依赖 list,Vue 会自动把这层链条串起来,任何一环变了,下游的缓存会依次作废。你只管声明"结果是什么",不用管"什么时候该重算"。
需要注意的是,缓存带来的是读取时的节省,不是"提前算好"。计算属性是惰性求值的:只有在真的被访问到(比如模板渲染到它、或者你在代码里读 .value)时才会第一次计算。如果一个计算属性从头到尾没人读,它的函数一次都不会跑。
三、要点二:计算属性要写纯函数,别在里面搞副作用
第二个要点是新手最常踩的坑:计算函数应该是一个纯函数——只根据依赖算出结果并返回,不要在里面改数据、发请求、操作 DOM。
反例长这样:
// 错误示范:在计算属性里改状态、发请求
const bad = computed(() => {
userInfo.value = fetchUser() // 副作用:改了别的响应式数据
sendLog() // 副作用:发请求
return userInfo.value.name
})
为什么这样写有问题?因为计算属性的执行时机由 Vue 决定:它可能在依赖变化时立刻算,也可能延迟到下次被访问时才算,甚至在严格模式下会被多算几次用来检查一致性。你把副作用塞进去,就相当于把业务逻辑的执行时机交给了框架的调度——什么时候发请求、发几次,全都不受你控制。更糟的是,副作用如果又改了计算属性的依赖,会形成"改依赖→重算→改依赖"的循环。
那异步请求该放哪?答案是 watch 或者事件处理函数。计算属性描述"某个值是什么",侦听器描述"某个值变了之后要去做什么",两者分工不同。想看具体的写法对照,Vue.js 教程 里对这两个 API 有完整示例。
同样的道理,也不要在计算属性里直接操作数组自身的破坏性方法后又依赖它。像 list.value.sort() 这种会原地修改数组的操作,放在计算属性里容易把依赖关系搅乱;要排序就先复制一份再排:
const sorted = computed(() => [...list.value].sort((a, b) => a.price - b.price))
四、要点三:依赖必须是响应式数据,否则永远不更新
第三个要点是缓存机制的"另一面",也是最难自查的一类 bug:如果计算函数里读的根本不是响应式数据,Vue 就收集不到任何依赖,于是缓存永远有效——结果永远不更新。
最经典的例子:
const now = computed(() => Date.now())
Date.now() 不是响应式数据,每次调用返回不同的值,但 Vue 无从知晓它变了。所以这个 now 在第一次被访问时算出一个时间戳,之后无论过多久、访问多少次,返回的一直是那个最初的时间戳。同理,Math.random()、new Date()、直接读一个普通(非 ref)的全局变量,全都不会触发更新。
这类问题的排查思路很直接:问自己一句"计算函数里读到的东西,是我用 ref / reactive / props 声明的吗?" 如果不是,那它就不在依赖追踪范围内。解决办法通常是把那个外部值也变成响应式的,或者在需要时手动触发更新:
import { ref, computed, onMounted, onUnmounted } from 'vue'
const now = ref(Date.now()) // 用一个 ref 承载时间
let timer = null
onMounted(() => {
timer = setInterval(() => { now.value = Date.now() }, 1000)
})
onUnmounted(() => clearInterval(timer))
const display = computed(() => new Date(now.value).toLocaleTimeString())
现在 display 依赖的是响应式的 now,每秒更新一次时间,display 才会跟着重算。这个改法把"时间会变"这件事显式地表达成了响应式状态,缓存机制就能正常工作了。
再补充一个细节:props 也是响应式的,所以基于 props 派生出一个新值,用计算属性完全没问题,而且这是官方推荐的用法——不要在子组件里用一个本地 data 去"复制" props,那样父组件更新时子组件不会跟着变。
五、怎么选:computed / methods / watch 决策清单
三个 API 都能"根据数据得到点什么",到底该用哪个?记住下面这张表就够:
| 场景 | 用谁 | 原因 |
|---|---|---|
| 从已有数据派生一个新值,要在模板里用 | computed |
有缓存,依赖不变不重算 |
| 要传参数才能算出结果 | methods |
计算属性不接受参数 |
| 数据变化后要执行副作用(发请求、写日志、改别的 state) | watch |
计算属性里写副作用是反模式 |
| 纯粹响应一个点击事件 | methods |
事件处理本身就是动作 |
一句话总结判断顺序:先看是不是"派生一个值"——是就用 computed;再看需不需要传参或执行动作——需要就退到 methods 或 watch。
还有个实用技巧:计算属性默认只读。如果你想让它可写(比如做一个带 getter/setter 的双向绑定),要显式写出 get 和 set:
const fullName = computed({
get() { return `${first.value} ${last.value}` },
set(val) {
const [f, l] = val.split(' ')
first.value = f
last.value = l
}
})
这个写法在封装表单组件时特别好用,但要注意:一旦写了 setter,就一定要保证 setter 里改的是响应式数据,否则又回到第四节那个"改了但不更新"的坑里。

总结
计算属性的缓存就三句话:依赖不变就复用结果,依赖一变立刻重算;计算函数要写成纯函数,副作用交给 watch;依赖必须是响应式数据,否则缓存永远不会失效、结果永远不更新。 把这三条记牢,绝大多数"computed 怎么不更新""computed 里发请求怎么不对"的问题都能自己定位。
- 缓存 = 以依赖为钥匙的复用,不是"只算一次";
- 计算属性里不写副作用、不发异步请求;
Date.now()/Math.random()不会被追踪,需要变化时先包一层ref;- 派生值用 computed,传参或执行动作用 methods / watch。
延伸学习
- Vue3 配套课程 —— 从组件基础一路讲到组合式 API 的实际用法。
- Vue 响应式与计算属性笔记 —— 把响应式黑洞、混入冲突等进阶陷阱一次列清。
- 在线运行 JavaScript 代码 —— 不用搭环境,直接验证本文的计算属性示例。
常见问题
Q:计算属性和方法到底差在哪?感觉写起来一样。
A:差别在调用方式和缓存。方法必须写成 total() 调用,每次调用都重新执行函数体;计算属性写成 total 直接访问,只有依赖变化时才重新执行。模板里如果同一个值被多处引用,用计算属性能明显减少重复计算。
Q:computed 里可以写 async / await 吗?
A:不建议,而且通常得不到你想要的结果。async 函数返回的是 Promise,计算属性拿到的是这个 Promise 对象而不是最终数据,模板渲染出来会是 [object Promise]。异步请放到 watch 或事件处理里,拿到结果后再写进响应式状态。
Q:为什么我改了对象里的某个字段,计算属性没更新?
A:先确认这个字段是不是响应式的。如果是用 reactive 声明的对象,新增一个原本不存在的字段在 Vue 2 里不会被追踪(Vue 3 的 reactive 基于 Proxy,新增字段可以追踪)。另外检查一下你是不是直接替换了整个对象却用了非响应式变量承接,或者读的是 Date.now() 这类根本不会被追踪的值。

免费 AI IDE



