先记住这个答案
computed 在 getter 执行时追踪响应式读取,并复用依赖未变化时的计算结果。相关依赖变化后,后续取值会按需要刷新结果;组件因为其它状态更新,并不意味着这个 computed 必须重新计算。依赖来自实际执行路径,普通时间函数或未经过响应式入口的变化不会自动让缓存更新。同时要区分 getter 重新求值和下游组件更新,派生结果仍相同时不应推导所有消费者都必须重做工作。
- 追踪 getter 实际读取的响应式来源
- 无关状态改变不自动使该计算结果失效
- 依赖失效、求值和下游更新是不同环节
无关输入更新不应扩大计算依赖
一个购物页面用 quantity 和 unitPrice 计算价格摘要,旁边还有搜索框状态。只要价格 getter 没有读取搜索词,搜索变化不应仅因同一个组件更新就成为价格计算的依赖。相反,如果 getter 无意中遍历了包含所有页面配置的对象,就可能扩大实际读取范围。
下面例子让 computed 返回一个新对象,通过结果引用比较观察缓存是否复用。无关 noise 改变后仍能读到同一份结果;quantity 变化后读取到新的摘要对象。业务代码不应依赖缓存对象身份作为永久实体 ID,这里只是用可观察行为解释缓存关系。
import { computed, reactive } from 'vue';
const state = reactive({ quantity: 2, unitPrice: 10, noise: 0 });
const summary = computed(() => ({ total: state.quantity * state.unitPrice }));
const first = summary.value;
const repeated = summary.value === first;
state.noise++;
const afterNoise = summary.value === first;
state.quantity = 3;
const afterQuantity = summary.value;
const result = { repeated, afterNoise, replaced: afterQuantity !== first, total: afterQuantity.total };Vue 3 中 result 的三个布尔值均为 true,total 为 30。通过结果引用观察缓存复用,没有在 getter 内修改计数器,也没有声明固定控制台输出。
条件分支会改变本轮读取的来源
例如价格 getter 根据 useDiscount 选择原价或折扣价,实际走到的分支决定本轮还读取哪些字段。分支条件本身也会被追踪;切换条件后重新执行 getter,依赖关系会按新的读取路径更新。不能仅凭函数源码里出现过某个字段,就认定它在所有时刻都触发这份计算。
Date.now 或普通外部变量没有自动提供响应式通知。如果要显示持续变化的时间,可以维护明确的时间 ref 并安排更新与清理,让 computed 从这个输入派生展示。不能把时间函数放进 computed 后期待缓存按照墙上时钟自然过期,也不能把它当成带有效期的网络缓存。
计算结果变化与消费者工作要分别观察
相关依赖改变意味着结果需要重新检查,但若重新求出的值与之前相同,下游工作不一定都需要重新执行。Vue 对 computed 稳定性的优化有具体版本边界;分析性能时应分别观察输入、getter 成本与消费者结果,避免把一个依赖通知等同于整片界面重绘。
若 getter 每次都返回新的对象,即使业务字段内容相同,也可能破坏结果稳定性。优化时先保持计算纯粹,再根据实际成本考虑返回值结构和稳定复用,不能为了复用旧对象而跳过本轮必要的依赖读取。渲染调度也不应被简化为每次赋值立即完成一次 DOM 更新。
容易答错的地方
- computed 就是只在组件第一次挂载时算一次
- 它会围绕响应式依赖重新求值,并不是固定初始化缓存。首次结果正确不代表之后不再更新,应通过修改实际输入验证计算是否仍与数据一致。
- 对象深处任何字段改变都会让 computed 失效
- 需要看 getter 实际读取了什么,以及变化是否经过响应式系统。只读取对象引用与读取某个深层属性的依赖范围不同,不能用深响应式概念替代具体读取分析。
面试官还会怎么问?
computed 从没被读取,也必须立即执行 getter 吗?
computed 具有按需计算的特征,不应依赖创建它时就执行某个副作用。需要执行外部同步时,应采用合适的侦听与生命周期机制,而不是把操作藏在 getter 中。
直接改 raw 对象能让 computed 自动失效吗?
绕过代理的写入不会自动经过响应式通知入口,因此不能依赖它触发计算更新。持续通过明确的代理或 ref 修改状态,才能让依赖关系与变化保持一致。
为什么不直接把普通方法当 computed 用?
方法调用按调用流程执行,不提供同样的响应式依赖缓存。是否需要 computed 取决于它是否是适合缓存的纯派生值,而不是因为代码能抽成函数就必须缓存。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。