很多朋友刚开始从 Vue2 切换到 Vue3,或者第一次接触组合式 API 的时候,都会对 computed 又爱又恨。爱的是它写起来简洁,恨的是有时候搞不清楚它到底在哪个时机计算、为什么能缓存、跟 watch 和 methods 的分工到底是什么。
我在项目里用了很长一段时间的 Vue3,从选项式 API 迁移到组合式 API 时,把 computed 相关的坑基本都踩了一遍。这篇就按我自己的理解,把 computed 的完整用法、原理和典型场景拆开揉碎了讲一遍。从简单的 getter/setter 语法,到源码层面看一下它怎么实现缓存,再到平时最容易翻车的异步和赋值问题,全部覆盖。最后还会给一份我在项目中实际用的选型清单和避坑手册,可以直接抄作业。
1. 内容整体设计与思路拆解
1.1 先搞清楚 computed 到底解决了什么问题
要理解 computed,先得回顾一个很基础的场景:页面上有一个变量,它的值需要由其他多个数据派生而来。不写 computed 的话,最直观的做法是写一个函数,每次需要的时候调用它,或者在模板里直接写表达式。这两种做法的问题在 Vue3 里都会被放大——模板表达式复杂了,维护起来很痛苦。
我举个实际例子来说明。订单页要展示一个“券后总价”,它的计算逻辑是:商品总价,加上邮费,减去优惠券抵扣,再按会员折扣做一次乘法。如果直接在模板里写:
<p>{{ (totalPrice + shippingFee - couponAmount) * (isMember ? 0.85 : 1) }}</p>这段代码能跑,但可读性差,而且跟处理逻辑混在一起。更麻烦的是,如果这个“券后总价”在页面的三个地方都要显示,你就要复制三遍同样的表达式。哪天规则变了,比如会员折扣从 85 折改成 8 折,你得找到所有模板位置逐个改,遗漏一处就是 bug。
computed 解决的就是这类“派生状态”问题:它让你把一段依赖其他响应式数据的计算逻辑封装成一个响应式引用。你把它当普通变量用,但它的值是自动追踪依赖、自动更新的。Vue 官方也一直在强调一个理念:模板里尽量只做简单的展示,任何稍微有点逻辑的值都应该用 computed 或 methods 提前处理。这个思想在 Vue3 里没有变,反而因为组合式 API 的灵活性,给了我们更大的发挥空间。
从设计思路上说,computed 最妙的地方在于它同时具备了函数和变量的优点。像 methods 里的函数那样有逻辑封装,可以多次复用;像 data 里的变量那样声明式地在模板中使用,还自带依赖追踪,不需要你手动关心“哪个数据变了要重新计算”。
1.2 computed 和 methods、watch 的职责边界
很多 Vue2 开发者容易陷入一个惯性:需要响应数据变化后做点什么,就上 watch。这个思维模式迁移到 Vue3 里往往会写出不必要的复杂代码。我在 code review 里看到过很多次类似这种情况:
// 反例:用 watch 维护派生数据 const fullName = ref('') watch([firstName, lastName], ([f, l]) => { fullName.value = f + ' ' + l })这段代码的问题在于:fullName 完全是由 firstName 和 lastName 派生出来的,你还在手动维护一份数据。一旦初始化顺序不对,或者 watch 的 flush 时机理解不透,页面渲染就会出现“滞后”的奇怪现象。正确做法很简单:
const fullName = computed(() => firstName.value + ' ' + lastName.value)两者对比能看出本质区别——computed 是声明式的,你描述的是“fullName 应该是什么”,至于它什么时候重算,Vue 自动处理。watch 是命令式的,你描述的是“firstName 或 lastName 变化之后,请帮我执行这段副作用代码”。什么时候该用 watch?答案是:当你在变化之后不是派生新数据,而是要做带有副作用的操作,比如发请求、操作 DOM、写入 localStorage,这个时候才轮到 watch 出场。
那 computed 和 methods 呢?直观上可能觉得“既然函数调用也能拿到计算值,何必要 computed”。这个理解在计算量小的时候确实没错,但一旦计算逻辑变重,区别就很明显了——methods 里的函数每次渲染都会重新执行,computed 则会缓存。这个缓存机制我会在源码部分重点展开,先说结论:只要是依赖响应式数据的纯计算,默认用 computed;只有当你确实需要每次都新鲜执行、或者不需要缓存的场景,才考虑 methods。
2. 核心语法与实操细节拆解
2.1 基础语法:getter 写法与只读 computed
Vue3 的 computed 最常用的写法是传一个 getter 函数,返回一个只读的 ref 对象。注意,这里说“只读”是重点——你不能直接给 computed 变量赋值,否则 Vue 会在开发模式下警告。
const count = ref(1) const doubleCount = computed(() => count.value * 2)用法上跟普通 ref 一样,在模板里直接使用变量名,在 JS 里用 .value 访问。这个设计简单直观,我项目中百分之八十的 computed 都是这种写法。有一个新手容易忽略的点:computed 返回的类型叫ComputedRef<T>,它确实是 Ref 类型的一种,但它的 value 属性是只读的。写类型的时候,你应该用 ComputedRef 而不是 Ref,这样 TypeScript 能在编译期就拦住你错误的赋值行为,避免运行时才发现问题。
getter 里面还有一个不能忽视的规则:必须是纯函数。也就是说,它不应该有副作用,不应该在 getter 里面修改其他响应式数据。可能有朋友会问,这不就是个硬性规定吗,写在里面会怎么样?我之前调试过一个诡异 bug,一个 computed 的 getter 里顺带递增了一个计数 ref,结果整个页面在无关数据更新时,计数会莫名其妙地跳动。原因就在于 computed 的 getter 执行次数和时机不完全等价于它的值被读取次数,加上缓存失效后依赖项变化可能在非渲染阶段触发求值,你的副作用会以你意想不到的频率发生。所以记住这条铁律:getter 里只做计算,不做事情。
2.2 setter 写法:可写 computed 的适用场景
computed 默认不可写,但你可以通过传入一个包含 get 和 set 的对象来创建一个可写的 computed。这种写法虽然不常用,但在某些场景下非常顺手。最典型的是双向绑定组件或者表单状态映射。
const firstName = ref('') const lastName = ref('') const fullName = computed({ get: () => firstName.value + ' ' + lastName.value, set: (val) => { const [first, ...rest] = val.split(' ') firstName.value = first lastName.value = rest.join(' ') } })这样你在模板里 v-model 绑定 fullName,就等于同时绑定了 firstName 和 lastName 的组合值;用户输入内容时,set 会解析字符串并更新两个源数据。这种写法本质上是用 computed 充当了一个“数据中间层”,把外部交互和内部存储结构解耦。
但可写 computed 要慎用,我一般只在以下两种情况使用:
- 子组件通过 v-model 同步一个内部派生状态给父组件
- 表单控件的显示值与数据模型格式不一致时,做一个转换层
除此之外,能不写 set 就尽量不写。因为 set 的存在意味着你的数据流方向变复杂了,原本“父数据 → 子数据”的单向流动,现在多了一条“子数据写回父数据”的路径。如果逻辑链路太长,出问题之后排查起来非常痛苦。
2.3 computed 和 ref、reactive 的数据访问差异
使用 computed 时最容易出错的点之一,是拿 computed 直接访问 reactive 对象的属性时,会意外触发响应式丢失的问题。
这里说一个具体的坑。假设你有:
const state = reactive({ list: [], keyword: '' }) const filteredList = computed(() => { return state.list.filter(item => item.name.includes(state.keyword)) })这在模板中使用filteredList是正常的,但是在 JS 的一个非渲染函数里访问呢?比如:
function logList() { console.log(filteredList.value) }这样用也是对的,因为当你写filteredList.value的时候,Vue 的运行时代码会先调用 track 收集依赖,再执行 getter 函数,所以 state.list 和 state.keyword 都会被追踪到,filteredList 会在它们变化时自动失效重算。但如果你在某个地方把filteredList.value解构出来存成普通变量,或者把它传给了某个没有响应式能力的第三方函数,那后续的更新自然就跟不上了。
const list = filteredList.value // 此时 list 是一个普通数组,跟响应式没有关系了这不是 computed 的问题,而是 JavaScript 引用语义和响应式机制的天然边界。我的建议是:只要在响应式环境里,就一直用 .value 访问 computed;一旦脱离响应式环境(传给非 Vue 逻辑、存为普通变量、或者用于不追踪依赖的回调中),就要意识到你已经“拍了一张快照”。很多“computed 不更新”的 bug 最后查下来都是这个原因,不是 computed 坏了,是引用链断了。
2.4 computed 跟上 TS 泛型与类型推导
Vue3 的项目基本都会使用 TypeScript,computed 的类型推导大多数情况下是自动的。比如:
const count = ref(1) const double = computed(() => count.value * 2) // double 的类型是 ComputedRef<number>严丝合缝,不需要你手动标注。但在一些边界情况下,类型推导可能会失真。最常见的是 getter 返回不同类型的联合类型,或者返回值为 null 的场景:
const data = ref<SomeData | null>(null) const processed = computed(() => { if (!data.value) return null return data.value.items.map(...) }) // processed 的类型是 ComputedRef<SomeData[] | null>这种联合类型在使用时总是需要判空,比较麻烦。如果你确定某个阶段数据一定存在,可以断言或做类型收窄,但如果你不确定,就诚实地保留联合类型,在模板里或调用方做空值处理。完整的处理我会在下面的场景章节里展开。
3. 缓存机制与响应式追踪原理
3.1 computed 的缓存是怎么实现的(源码视角)
computed 的缓存机制是它区别于方法的根本原因,但很多教程只讲“有缓存”这个结论,不讲原理。我基于 Vue3 源码(核心在packages/reactivity/src/computed.ts)说一下我的理解,虽然不逐行粘贴,但核心逻辑能讲明白。
computed 内部维护了三个关键状态:
_value:缓存的值_dirty:标记是否需要重新计算_cacheable:是否启用缓存
初始化时_dirty为 true,当你第一次读取.value时,由于_dirty为 true,会执行 getter,计算出结果存入_value,然后把_dirty设为 false,最后把_value返回。
那_dirty什么时候变回 true?答案在响应式依赖的更新流程里。computed 的 getter 执行时,它会通过 track 收集内部依赖(比如 count.value);当 count 触发更新时,computed 实例上注册的 effect 会被触发,此时执行调度器,把_dirty设置为 true,并通知 computed 的所有外层依赖重新读取。外层依赖读取时,发现_dirty为 true,于是重新执行 getter,更新_value。
这就是缓存的核心:依赖没变,_dirty一直是 false,每次读取直接返回_value,根本不执行 getter。依赖变了,_dirty置为 true,但也不是立刻计算,而是等到下一次被读取时才重新计算。这个“懒计算”的设计非常精妙,既避免了不必要的计算,也保证了读取到的一定是最新值。
上面说了_cacheable,它在什么情况下会变 false 呢?当 computed 的依赖项是异步更新的,或者当某个 effect 显式标志了不接受缓存时(比如 watchEffect 里使用了flush: 'sync'),computed 的缓存就会失效退化为每次重新求值。但在正常业务代码中我们基本接触不到这个分支,所以不需要特别去调它,知道存在即可。
3.2 依赖收集和派发更新的完整链路
理解了缓存实现,我们再从整体上看一遍 computed 在响应式系统里的位置。Vue3 的响应式系统核心是 effect、track 和 trigger 三件套。computed 本质上是一个被特殊化处理的 effect,可以叫它“懒 effect”。
链路是这样的:
- 你在模板里写
{{ doubleCount }} - 渲染 effect 被创建,读取 doubleCount.value
- 这一步触发了 computed 实例的 track 逻辑,渲染 effect 被注册为 computed 的依赖
- 同时 computed 因为 _dirty 为 true,开始执行 getter,getter 内部读取 count.value
- count 的 track 逻辑把 computed 的 effect 注册为 count 的依赖
- 于是形成一条链:count → computed effect → 渲染 effect
当 count.value 变化时,触发 count 的依赖,也就是 computed effect。computed effect 的调度器把 _dirty 设为 true,然后触发它自己的依赖——渲染 effect。渲染 effect 重新执行,读取 doubleCount.value,发现 _dirty 为 true,重新执行 getter,拿到最新值,完成渲染。
这条链解释了 computed 的一个行为特性:computed 的更新是“连锁反应”的,而不是在依赖变化瞬间就立刻重算。它是在渲染进程真正需要读取它的时候才重算。理解这一点之后,很多关于“computed 更新时机”的困惑就迎刃而解了。Vue3 的响应式性能之所以好,跟这种懒计算设计是分不开的——它把大量计算延迟到最后一刻,能算就算一次,绝不重复。
3.3 滥用 computed 导致的性能隐患
这里要讨论一个反向问题:computed 用错了也可能成为性能瓶颈。核心矛盾在于,computed 的 getter 虽然“懒”,但一旦它的任何依赖项发生变化,它在下次被读取时就会重新完整执行一遍。
举个例子:
const items = ref(generateLargeArray()) const keyword = ref('') const filteredItems = computed(() => { return heavyFilter(items.value, keyword.value) })heavyFilter 是一个需要遍历十万条数据的复杂过滤函数。用户每次输入一个字符,keyword 变化,filteredItems 失效;渲染时重新执行过滤函数,对十万条数据重新遍历。如果输入速度很快,计算成本累计起来就会卡顿。
这种场景下,computed 的缓存并没有帮上太大忙,因为输入本身就在高频变化。更优的方案是:
- 如果过滤逻辑支持防抖,把关键字输入源做成防抖 ref
- 如果数据量确实大,可以配合
watch+ 定时器做异步重算 - 更极端的方案是做虚拟滚动,避免一次性渲染大量 DOM
computed 适用于“依赖低频变化、计算有一定成本”的场景;如果依赖高频变化且计算量又大,需要在前置环节(比如输入防抖)做处理,而不是指望 computed 的缓存。这是我在实际项目里体会最深的一点。
4. 应用场景、进阶实战与避坑指南
4.1 在模板和 JS 中使用 computed 的完整示例
一个相对完整的综合示例,把前面讲的语法要点串起来:
<script setup> import { ref, reactive, computed } from 'vue' const products = ref([ { name: 'A', price: 100, count: 2, selected: true }, { name: 'B', price: 50, count: 3, selected: false } ]) const couponAmount = ref(20) const isMember = ref(true) // 商品小计 const subTotal = computed(() => { return products.value.reduce((sum, p) => { if (!p.selected) return sum return sum + p.price * p.count }, 0) }) // 计算邮费:满 99 包邮,否则收 8 块 const shippingFee = computed(() => { return subTotal.value >= 99 ? 0 : 8 }) // 会员折扣 const discountFactor = computed(() => isMember.value ? 0.85 : 1) // 最终价格 const finalTotal = computed(() => { return (subTotal.value * discountFactor.value + shippingFee.value) - (couponAmount.value > 0 ? couponAmount.value : 0) }) </script>这种写法把计算逻辑一层层拆开,每个 computed 只做一件事,后续无论是排查问题还是调整规则,都能精准定位。而且因为 computed 可以依赖另一个 computed,这种“计算链”可以组织得非常清晰。但有一点要注意:计算链的层级不要过深。我自己定的一个标准是,computed 之间的依赖层级超过三层,就要考虑是不是该把中间逻辑抽成函数或者直接改用一个 store(状态管理库)。层级太深,虽然结果没错,但排查起来心智负担很重。
在模板中,直接使用这些 computed 变量,不需要加 .value:
<p>商品小计:{{ subTotal }}</p> <p>运费:{{ shippingFee === 0 ? '包邮' : shippingFee }}</p> <p>最终价格:{{ finalTotal }}</p>一个易混淆点是:模板里不要写subTotal.value,也不要写subTotal()这样调用。Vue 模板编译器对 ref 做了自动解包,直接写变量名即可。如果你写的表达式带了括号,说明你心里还在把它当成函数调用,这是 Vue2 思维方式残留,要扭转过来。
4.2 多个 computed 的写法和组织模式
在实际项目中,我推荐一个组织模式:把一组相关的计算逻辑放在一起,形成“派生数据模块”。比如在一个列表页里:
const list = ref([]) const filter = ref('all') const searchText = ref('') const filteredByStatus = computed(() => { if (filter.value === 'all') return list.value return list.value.filter(item => item.status === filter.value) }) const visibleList = computed(() => { const keyword = searchText.value.trim().toLowerCase() if (!keyword) return filteredByStatus.value return filteredByStatus.value.filter(item => item.name.toLowerCase().includes(keyword)) }) const sortedList = computed(() => { return [...visibleList.value].sort((a, b) => b.createdAt - a.createdAt) }) const totalCount = computed(() => sortedList.value.length) const currentPageList = computed(() => { const start = (page.value - 1) * pageSize.value return sortedList.value.slice(start, start + pageSize.value) })每一步都比前一步更接近“视图想要的形状”,每一层只做一件事。这样做的最大好处是,视图代码永远只依赖最后一个 computed(currentPageList),前面的计算逻辑全部封闭在其他 computed 里。测试时也可以针对单独的 computed 做单元测试,输入响应式数据,断言结果。
这种模式在数据流复杂度较高的页面里非常有效。我在一个数据可视化大屏项目里,把整个页面拆成了五个这样的“衍生层”,每一层都有独立的含义,后来换人或接手都很容易理解。
4.3 computed 内使用 async/await 的致命问题
在继续看具体场景前,先铺一个很多人都会踩的大坑。有朋友写:
const data = computed(async () => { const res = await fetch(...) return res.data })这段代码的问题非常隐蔽:一个 async 函数无论如何都会返回一个 Promise。computed 执行这个 getter 后,得到的缓存值就是一个 Promise 对象,而不是真正的数据。在模板里你永远等不到数据,只能看到[object Promise]。而且 computed 只会在依赖数据变化时重新执行 getter,在 Promise resolve 之后,它并不会得到通知,也不会更新视图。
正确做法是配合 ref + watch 来处理异步派生状态。比如:
const userId = ref(1) const userData = ref(null) watch(userId, async (id) => { userData.value = null userData.value = await fetchUser(id) }, { immediate: true })computed 是同步派生状态的工具,它的值必须是同步可得的。异步请求、定时器、DOM 操作都要放 watch 或者事件回调里面。理解了这条边界,就不会写出“computed 不更新”的疑难 bug。
4.4 处理 props 传入的响应式数据
在组件开发中,computed 经常被用来处理 props。这个场景里有几个高频问题。
第一个问题:props 是父组件传入的普通响应式对象,你在子组件里可以直接基于 props 写 computed:
const props = defineProps({ items: { type: Array, required: true }, unitPrice: { type: Number, required: true } }) const totalPrice = computed(() => { return props.items.reduce((sum, item) => sum + item.price * props.unitPrice, 0) })注意 computed 里访问的是 props.xxx,不能解构 props 再访问。如果你在顶层写了const { items } = props,然后 computed 里用items,items 就是一个普通变量,后续父组件数据更新时 computed 无法感知到变化。Vue3 官方提供了toRefs或者toRef来解决解构后的响应式丢失问题,不过既然 computed 可以直接访问 props,我用得最多的是直接props.xxx。
第二个问题:基于 props 创建一份要修改的副本。props 本身不应该被直接修改,但有时我们需要对传入的数据做展示层面的变化,比如给每个 item 加一个选中标记。正确方式是把 props 复制到一个 ref,再由 computed 派生:
const localItems = ref(props.items.map(item => ({ ...item, selected: false }))) const selectedCount = computed(() => { return localItems.value.filter(item => item.selected).length })这里有一个陷阱:浅拷贝只是复制了第一层。如果 item 内部还有嵌套对象,它们仍然引用原对象地址,修改嵌套字段仍然会影响父组件的 props 数据。我一般这样处理:如果真的需要完全独立的数据副本,用深拷贝,比如structuredClone或 JSON 序列化。如果确认父组件不会因为子组件同步修改而受影响,浅拷贝也行,但要心里有数这股数据流是共享的。
4.5 配置项 global 与调试技巧(沿用 Vue2 经验迁移)
Vue3 虽然没有了 Vue2 里的computed配置项(选项式 API 的 computed 现在也是写在setup()返回对象中或<script setup>中),但很多从老项目迁移的朋友还是容易把两者混淆。
在选项式 API 中,computed 是这样写的:
export default { data() { return { count: 1 } }, computed: { double() { return this.count * 2 } } }在 Vue3 里如果你仍然使用选项式 API,这种写法可以继续用,Vue3 是兼容的。但如果你用了<script setup>组合式 API,就不能把这个配置项搬过来,需要改成:
const count = ref(1) const double = computed(() => count.value * 2)从选项式 API 迁移时,我总结了一个简单的对应关系:data 的字段对应 ref/reactive,computed 配置项的每个 key 对应一个 computed 调用,methods 的函数对应普通函数,watch 配置项对应 watch/watchEffect。
4.6 调试技巧:为什么 computed 在 DevTools 里不显示/不更新
实际开发中,调试 computed 最常用的工具是控制台日志和 Vue DevTools。
如果你在 getter 里放 console.log,它有可能会重复执行,这会让人困惑。原因在前面原理部分讲过:读取 computed.value 会触发计算,依赖变化后再次读取也会触发计算。不要在 getter 里放 console.log 当作调试手段,因为它的执行次数跟“被读取次数”正相关,并不等价于依赖变化次数。如果你真的需要观察 computed 的重算时机,可以这样写:
const myComputed = computed({ get() { console.log('computed 重新计算') return someRef.value * 2 }, set() {} })这种方式至少能区分读取和计算。不过项目代码里最好把这类调试日志移除,不要留线上。
在 DevTools 中,组合式 API 里的 computed 变量会在 setup 组件的数据面板中以ComputedRef类型显示,展开可以看到value和effect等内部字段,但不能直接修改 value。有些朋友在 DevTools 里手动修改 computed 的 value 发现改不动,这是设计使然——computed 的 value 是只读的,DevTools 也遵循这个约束。如果你想验证数据更改后的效果,应该修改 computed 依赖的源数据。
4.7 常见问题与排查技巧实录
这一节把我在项目中遇到的高频问题整理成速查表,每个问题后面附上排查思路。
| 现象 | 大概率原因 | 排查方向 |
|---|---|---|
| computed 的值不更新 | 在 getter 中访问了被解构的 props 或普通变量 | 检查依赖的响应式链是否断了 |
| computed 里访问不到最新的 props | 解构 props 后丢失响应式 | 改回props.xxx形式访问 |
| computed 返回 Promise 对象 | getter 里使用了 async/await | 改用 ref + watch 异步模式 |
| computed 的值在模板里显示 undefined | getter 返回了 undefined,或者依赖的数据还没有赋值 | 在 getter 里做空值兜底 |
| computed 赋值报错 | 非可写 computed 被赋值 | 使用 get/set 写法或不要赋值 |
| computed 执行次数过多 | 依赖了多个高频变化的响应式数据 | 检查变化源头,考虑防抖或拆分 |
| 多个组件共用 computed 状态 | 组件各自定义,数据不同步 | 把 computed 提升到共享 store 层级 |
| computed 里修改了其他响应式数据 | 违反了 getter 纯函数原则 | 把副作用移到 watch 或事件回调 |
4.8 项目中的最终选型实战建议(含速查清单)
把所有内容串起来,我在自己的团队里定了这么一套选型标准,基本覆盖了日常开发百分之九十九的情况:
- 模板表达式超过一个三元表达式,就改写成 computed
- 多个数据源派生一个展示值,用 computed
- 计算逻辑需要在多处复用,用 computed 或抽成组合式函数
- 响应式数据变化后需要执行副作用(请求、写存储、操作 DOM),用 watch
- 需要防抖/节流地监听数据变化,用 watch 配合定时器,不要用 computed
- 数据变化后需要异步获取新数据,用 watch + ref 模式,禁用 async computed
- 组件 props 的派生展示状态,用 computed
- 需要给组件绑定一个可写的派生状态(v-model 代理),用 get/set 写法
- 纯粹的函数计算、不依赖响应式数据,用普通 function,不要用 computed
最后还有一个习惯建议:在写 computed 的时候,先把依赖列出来,确认每一个依赖都是响应式引用(ref、reactive、props 的属性或另一个 computed),再开始写 getter。这个习惯帮我避开了大量“改着改着不更新”的坑,因为在 Vue3 的响应式系统中,所谓的依赖,本质上是 getter 执行时读取到的那些响应式变量,只要你用 .value 或 props.xxx 的方式访问,一切就都在链路上。
结尾(个人经验收尾,不写总结)
我跟 computed 磨合了挺长时间,踩过的坑说多不多,说少不少。最深的体会有两条:一条是任何时候都要记住 computed 是“纯计算 + 缓存”的产物,一旦你发现自己在 computed 里做请求、做赋值、做异步操作,先停下来想想是不是选错了工具。另一条是遇到“computed 不更新”,不要怀疑代码逻辑,先检查响应式依赖链路是不是断了,九成的问题是解构或者引用转型导致的。
另外再分享一个我自己的使用习惯:我会把页面里所有跟“派生展示值”相关的 computed 统一放在一个区块,用注释打上分组标号。时间长了项目大了,回来看代码时,哪个值由哪些源数据派生而来一目了然,改逻辑的时候也更不容易误伤。这个习惯不一定适合所有人,但对维护期长的项目来说,确实能省下不少时间。