目录
- [1. 为什么用 computed](#1. 为什么用 computed)
- [2. 基础用法(在 setup 中)](#2. 基础用法(在 setup 中))
- [3. 不传参数的 computed(最常见)](#3. 不传参数的 computed(最常见))
- [4. 传参数的 cmputed:](#4. 传参数的 cmputed:)
-
-
- [模式 A --- 工厂函数(推荐用于可复用、参数在创建时确定)](#模式 A — 工厂函数(推荐用于可复用、参数在创建时确定))
- [模式 B --- computed 返回函数(如果需要在运行时传入参数)](#模式 B — computed 返回函数(如果需要在运行时传入参数))
-
- [5. 只读 vs 可写(getter / setter)](#5. 只读 vs 可写(getter / setter))
-
-
- [只读 computed(最常见)](#只读 computed(最常见))
- [可写 computed(带 getter 和 setter)](#可写 computed(带 getter 和 setter))
-
- [6. computed 的缓存、惰性求值与依赖追踪(原理要点)](#6. computed 的缓存、惰性求值与依赖追踪(原理要点))
- [7. 与 `ref` / `reactive` / `toRef` / `toRefs` 的配合使用](#7. 与
ref/reactive/toRef/toRefs的配合使用) -
-
- [reactive 对象与 computed](#reactive 对象与 computed)
- [组合示例:参数化工厂 + toRef](#组合示例:参数化工厂 + toRef)
-
- [8. computed 与 watch / watchEffect / methods 的对比与选择](#8. computed 与 watch / watchEffect / methods 的对比与选择)
- [9. 异步计算:为什么不能直接写 async computed?怎么做?](#9. 异步计算:为什么不能直接写 async computed?怎么做?)
-
-
- [推荐做法 A --- 使用 `ref + watch` 或 `watchEffect`](#推荐做法 A — 使用
ref + watch或watchEffect) - [推荐做法 B --- 如果想"声明式"一点,可返回一个函数或封装成 composable:](#推荐做法 B — 如果想“声明式”一点,可返回一个函数或封装成 composable:)
- [推荐做法 A --- 使用 `ref + watch` 或 `watchEffect`](#推荐做法 A — 使用
-
- [10. 高级场景(实战示例)](#10. 高级场景(实战示例))
-
-
- [场景 A:表单实时校验(computed + watch)](#场景 A:表单实时校验(computed + watch))
- [场景 B:列表搜索与分页(computed 做过滤 + 排序)](#场景 B:列表搜索与分页(computed 做过滤 + 排序))
- [场景 C:组件双向绑定(v-model)使用 computed getter/setter](#场景 C:组件双向绑定(v-model)使用 computed getter/setter)
- [场景 D:在 Pinia / Store 中用 computed(getter)](#场景 D:在 Pinia / Store 中用 computed(getter))
-
- [11. 性能优化策略与常见陷阱](#11. 性能优化策略与常见陷阱)
- [12. 单元测试要点](#12. 单元测试要点)
- [13. SSR / Hydration 注意事项](#13. SSR / Hydration 注意事项)
- [14. 常见问题与解答(FAQ)](#14. 常见问题与解答(FAQ))
- [15. 实用 patterns(代码片段合集)](#15. 实用 patterns(代码片段合集))
-
-
- 1) 工厂 composable:传参返回 computed 工厂 composable:传参返回 computed)
- 2) computed 返回函数(可被多个参数调用) computed 返回函数(可被多个参数调用))
- 3) v-model 封装(getter/setter) v-model 封装(getter/setter))
- 4) TypeScript 中指定类型 TypeScript 中指定类型)
- 5) 防抖搜索(watch + debounce) 防抖搜索(watch + debounce))
-
- [16. 更深一层:EffectScope 与 computed 生命周期](#16. 更深一层:EffectScope 与 computed 生命周期)
- [17. 文章结语(总结与实践清单)](#17. 文章结语(总结与实践清单))
- [18. 快速参考 Cheat Sheet](#18. 快速参考 Cheat Sheet)
1. 为什么用 computed
computed 的核心价值:派生状态(derived state) + 缓存(memoization)。它适合"从已有响应式数据推导出新数据"的场景。
举例:
- 把
firstName+lastName合并成fullName - 根据
cart.items计算totalPrice - 复杂的筛选/排序结果(只在依赖变化时重算)
如果用 methods 每次模板渲染都会调用,成本高且无缓存;如果用 watch 写派生值,逻辑不直观且需要处理初始值/边界。computed 将这些问题优雅解决。
2. 基础用法(在 setup 中)
最基础的写法:
html
<script setup>
import { ref, computed } from 'vue'
const count = ref(0)
const double = computed(() => count.value * 2)
</script>
<template>
<div>
<p>{{ count }}</p>
<p>{{ double }}</p> <!-- 在 template 中直接访问,不用 .value -->
<button @click="count++">+1</button>
</div>
</template>
注:
- 在
<script setup>或普通<script>中:在 JS 里取值用.value,在模板里直接用变量名(Vue 会自动解包 ref)。 computed返回一个Ref(ComputedRef),在 JS 中取值仍需.value。
3. 不传参数的 computed(最常见)
不传参数意味着 computed 的 getter 只依赖封闭作用域里的响应式变量。
示例 1:合并姓名
js
const firstName = ref('Rocky')
const lastName = ref('Peng')
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
示例 2:购物车总价
js
const cart = ref([
{ id: 1, price: 10, qty: 2 },
{ id: 2, price: 5, qty: 1 }
])
const totalPrice = computed(() => cart.value.reduce((s, i) => s + i.price * i.qty, 0))
这些都是典型的"不传参数"场景:computed 的值完全由其闭包内的响应式源决定。
4. 传参数的 cmputed:
传参数主要有两种模式(工厂函数 / 返回函数)。
有时希望根据不同参数生成/使用 computed,有两种常见做法:
模式 A --- 工厂函数(推荐用于可复用、参数在创建时确定)
把参数变成 ref 或 reactive,传入 composable,返回 computed:
js
function useFilteredList(listRef, keywordRef) {
return computed(() =>
listRef.value.filter(item => item.name.includes(keywordRef.value))
)
}
// 使用
const list = ref([{ name: 'apple'}, {name: 'banana'}])
const keyword = ref('app')
const filtered = useFilteredList(list, keyword) // filtered.value 会根据 keyword 变
优点:依赖明确、缓存仍然有效、易于测试。
模式 B --- computed 返回函数(如果需要在运行时传入参数)
有时想要一个"可调用"的计算属性:
js
const items = ref([...])
const findById = computed(() => (id) => items.value.find(i => i.id === id))
// 使用
const item = findById.value(123)
注意点:
findById本身是个Ref,它的.value是一个函数。- 这个函数内部仍然会使用
items.value(即使每次传入不同id,缓存机制只缓存函数本身,而不是"函数对某个参数的结果")。如果函数内部很重,传入不同参数时不会自动缓存每个参数的结果。
如果需要对不同参数的"结果"进行缓存(memoize),需要自己实现 memoization(例如 Map 缓存)或使用外部库。
5. 只读 vs 可写(getter / setter)
computed 支持两种写法:
只读 computed(最常见)
js
const doubled = computed(() => count.value * 2)
只包含 getter,没有 setter;直接写 doubled.value = x 会抛出警告(in development)或无效。
可写 computed(带 getter 和 setter)
适合需要把内部 state 暴露为 v-model 绑定时使用:
js
const message = ref('hello')
const writable = computed({
get: () => message.value,
set: (val) => { message.value = val.trim() }
})
常见场景:在子组件中做 v-model 的封装:
html
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const inner = computed({
get: () => props.modelValue,
set: (v) => emit('update:modelValue', v)
})
</script>
<template>
<input v-model="inner" />
</template>
这是非常典型也非常实用的模式。
6. computed 的缓存、惰性求值与依赖追踪(原理要点)
核心行为(简要版):
computed的 getter 惰性(lazy) :只有当读取.value(或模板访问)时才会执行 getter。- 缓存:第一次读取 getter 计算并缓存结果,后续读取只返回缓存,直到某个依赖发生变化(被触发),此时缓存被标记为"脏",下一次读取才会重新计算。
- 依赖被自动追踪 :在 getter 执行期间访问的任何
ref/reactive都会被加入依赖列表(effect dependency tracking)。
结论与实践:
computed适合昂贵的计算(可以避免重复)- 依赖必须在 getter 中被显式访问,否则不会被追踪
- 在模板中多次使用 computed 性能开销极小(因为缓存)
7. 与 ref / reactive / toRef / toRefs 的配合使用
reactive 对象与 computed
如果用 reactive(obj),在 computed 内访问 obj.prop 就能追踪该属性:
js
const state = reactive({ a: 1, b: 2 })
const sum = computed(() => state.a + state.b)
注意的坑 :如果把 reactive 的对象解构成普通变量(const { a } = state),会丢失响应性 。正确的做法是 toRefs 或 toRef。
js
// 错误:失去响应性
const { a } = state
const sum = computed(() => a + 1) // a 不是 ref
// 正确:保持 reactivity
const { a } = toRefs(state)
const sum = computed(() => a.value + 1)
在 setup 返回模板时,如果想保留响应性,通常直接返回 state(reactive)或用 toRefs(state) 返回所有的 refs。
组合示例:参数化工厂 + toRef
js
function useItemTotal(itemRef) {
return computed(() => itemRef.value.price * itemRef.value.qty)
}
const cart = reactive({ items: [{id:1, price:10, qty:2}] })
const item0 = toRef(cart.items, 0) // 注意:toRef 在数组索引上用法有局限
通常在数组上我们仍然使用 ref 包裹数组或直接使用 computed 结合索引来访问。
8. computed 与 watch / watchEffect / methods 的对比与选择
computed:声明式、缓存、无副作用(不该在里面做副作用),适合"派生数据"。watch:响应式副作用,可以监听一个或多个响应式源并在变化时执行副作用(异步/同步都可以),适合 API 调用、数据上报、手动更新非响应式值等。watchEffect:类似watch,但在初始化时会立即执行一次 getter,自动收集依赖。它更适合自动执行的副作用。
示例:如果要根据 keyword 发请求获取结果,不要在 computed 里做网络请求(那是副作用),应使用 watch / watchEffect:
js
const keyword = ref('')
const results = ref([])
watch(keyword, async (k) => {
results.value = await fetchSearch(k)
})
9. 异步计算:为什么不能直接写 async computed?怎么做?
可以写 computed(async () => {...}),但它不会按希望那样工作(它会返回 Ref<Promise<T>>,且模板无法等待 Promise 自动更新)。更好的模式:
推荐做法 A --- 使用 ref + watch 或 watchEffect
js
const param = ref('init')
const data = ref(null)
const loading = ref(false)
const error = ref(null)
watch(param, async (p, old) => {
loading.value = true
error.value = null
try {
data.value = await fetchData(p)
} catch (e) {
error.value = e
} finally {
loading.value = false
}
}, { immediate: true })
推荐做法 B --- 如果想"声明式"一点,可返回一个函数或封装成 composable:
js
function useAsyncData(getterRef) {
const data = ref(null)
const loading = ref(false)
watch(getterRef, async (g) => {
loading.value = true
data.value = await g()
loading.value = false
}, { immediate: true })
return { data, loading }
}
第三方工具(如 @vueuse/core)有 useAsyncState 等函数可以更简洁,但核心思路还是 ref + watch。
10. 高级场景(实战示例)
下面给出几个具体实战场景的完整代码片段。
场景 A:表单实时校验(computed + watch)
需求:当表单某字段变化,实时显示错误提示(但不要把副作用放到 computed)
js
<script setup>
import { ref, computed, watch } from 'vue'
const form = ref({
username: '',
email: ''
})
const usernameError = ref(null)
watch(() => form.value.username, (val) => {
if (!val) usernameError.value = '用户名必填'
else if (val.length < 3) usernameError.value = '用户名至少 3 个字符'
else usernameError.value = null
})
const isFormValid = computed(() => !usernameError.value && !!form.value.email)
</script>
<template>
<input v-model="form.username" />
<p v-if="usernameError">{{ usernameError }}</p>
<p>表单是否有效:{{ isFormValid }}</p>
</template>
说明:把校验逻辑放到 watch(副作用),把整体验证结果作为 computed(派生数据)。
场景 B:列表搜索与分页(computed 做过滤 + 排序)
js
const rawList = ref([...]) // 原始数据
const keyword = ref('')
const sortBy = ref('name') // 'name' | 'date' 等
const page = ref(1)
const pageSize = 10
const filtered = computed(() =>
rawList.value.filter(item => item.name.includes(keyword.value))
)
const sorted = computed(() =>
filtered.value.slice().sort((a, b) => a[sortBy.value].localeCompare(b[sortBy.value]))
)
const paged = computed(() => {
const start = (page.value - 1) * pageSize
return sorted.value.slice(start, start + pageSize)
})
要点:
- 组合多个 computed(filtered -> sorted -> paged)可以保持清晰,并保留缓存效果。
sorted使用slice()创建副本,避免改变原 array。
场景 C:组件双向绑定(v-model)使用 computed getter/setter
js
<script setup>
const props = defineProps({ modelValue: String })
const emit = defineEmits(['update:modelValue'])
const model = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
</script>
<template>
<input v-model="model" />
</template>
场景 D:在 Pinia / Store 中用 computed(getter)
Pinia 的 getters 本质上就是 computed:
js
// store.js
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({ items: [] }),
getters: {
totalPrice: (state) => state.items.reduce((s, i) => s + i.price * i.qty, 0)
}
})
// 使用
const cartStore = useCartStore()
cartStore.totalPrice // 直接访问
在组件里,如果要基于 store 的派生数据再做计算,可以在组件中用 computed(() => cartStore.totalPrice * 0.9);或者直接在 store 写更复杂的 getters。
11. 性能优化策略与常见陷阱
性能建议
- 把昂贵运算放进 computed,避免每次渲染重复计算。
- 拆分 computed :小而专一的 computed 更容易缓存和组合(例如
filtered、sorted、paged分开)。 - 避免在 getter 中做副作用或长时间阻塞(网络请求、同步 I/O 等)。
- 在大型列表上使用虚拟滚动(virtual scroll),computed 的结果即便缓存但渲染大量 DOM 仍然慢。
- 对于高频变化但 UI 仅需要延迟更新的场景,使用防抖(debounce)策略 :通常通过
watch+ 防抖实现,而不是试图在 computed 里做防抖。
常见陷阱(坑)
- 解构 reactive 丢失响应性 :
const { a } = reactiveObj会丢失 reactivity;应使用toRefs。 - 在 computed 中写副作用:computed 不适合写副作用(会导致不可预测行为)。
- 误以为 computed 会缓存"带参数的调用结果":computed 会缓存 getter 的返回值,而不是缓存"函数对每个参数的返回值"。
- async computed 的误用:不要把异步副作用放在 computed;应使用 watch/wathEffect。
- 把大量数据变成 computed,而不是按需 :有时候直接在渲染函数中使用
ref并按需处理比创建大量 computed 更佳(看具体场景)。
12. 单元测试要点
测试 computed 本身很简单:改变依赖后断言其 .value 变化。
示例(使用 vitest 或 jest + @vue/test-utils):
js
import { ref, computed } from 'vue'
import { describe, it, expect } from 'vitest'
it('computed updates when dependency changes', () => {
const r = ref(1)
const d = computed(() => r.value * 2)
expect(d.value).toBe(2)
r.value = 3
expect(d.value).toBe(6)
})
如果测试组件中的 computed,推荐使用 mount(),修改 props/ref 后调用 await nextTick() 再断言 DOM 或 computed。
13. SSR / Hydration 注意事项
- Computed 在服务端渲染时也会执行(因为它只是同步计算)。因此 不要在 computed 中依赖浏览器专有 API(如
window)。 - 任何在服务端和客户端可能产生不同结果的 computed 都会导致 hydration mismatch(页面被重新渲染或者报错)。保持计算的确定性(deterministic)是关键。
- 如果 computed 依赖的是仅客户端可用的数据(比如 localStorage、随机数等),需要在客户端延迟计算或用
onMounted初始化。
14. 常见问题与解答(FAQ)
Q1:computed 会每次模板渲染都执行吗?
A:不会。只有当读取 .value 且缓存脏(依赖变化)时才会重新计算。简单的读取不会重复计算已缓存结果。
Q2:computed 与 methods 哪个性能更好?
A:如果逻辑要被多次读取且有一定成本,computed 更好(因为缓存)。如果逻辑是事件触发并且不被模板重复读取,methods 可以更直观。
Q3:如何让 computed 支持参数?
A:两种方式:工厂函数(在创建时传入参数)或 computed 返回函数供调用。若需要按参数缓存返回值,需要自己实现 memoization。
Q4:可以在 computed 里做异步请求吗?
A:技术上可以写 computed(async () => ...),但它返回 Ref<Promise> 并不能像普通 computed 一样工作。推荐使用 ref + watch 或 watchEffect。
Q5:computed 会自动追踪深层属性吗?
A:只要在 getter 中访问了那层属性,Vue 会追踪它。不访问就不会追踪。例如 computed(() => state.obj.nested.value) 会追踪 nested。注意数组索引或动态 key 的追踪:访问时会追踪。
15. 实用 patterns(代码片段合集)
1) 工厂 composable:传参返回 computed
js
export function useItemValue(itemsRef, idRef) {
return computed(() => itemsRef.value.find(i => i.id === idRef.value))
}
2) computed 返回函数(可被多个参数调用)
js
const findUser = computed(() => (id) => users.value.find(u => u.id === id))
3) v-model 封装(getter/setter)
js
const props = defineProps(['modelValue'])
const emit = defineEmits(['update:modelValue'])
const model = computed({
get: () => props.modelValue,
set: v => emit('update:modelValue', v)
})
4) TypeScript 中指定类型
ts
import { computed, ref } from 'vue'
const count = ref<number>(0)
const double = computed<number>(() => count.value * 2)
5) 防抖搜索(watch + debounce)
js
import { ref, watch } from 'vue'
import { debounce } from 'lodash-es'
const keyword = ref('')
const results = ref([])
const doSearch = debounce(async (k) => {
results.value = await searchApi(k)
}, 300)
watch(keyword, (k) => doSearch(k))
16. 更深一层:EffectScope 与 computed 生命周期
在复杂应用或插件中,可能会在 composable 内使用 effectScope 创建可释放的响应式副作用。computed 也会与当前 effectScope 关联。如果想在某个时刻销毁这些 effect(例如组件卸载或手动 cleanup),需要注意 computed 的创建上下文。
示例(较少见但有用):
js
import { effectScope, ref, computed } from 'vue'
const scope = effectScope()
const { run, stop } = scope
run(() => {
const a = ref(1)
const b = computed(() => a.value * 2)
// ...
})
// later
scope.stop() // 会释放内部创建的副作用 / computed
17. 文章结语(总结与实践清单)
computed 在 Vue 中是非常基础也非常强大的工具。总结一下实践要点:
- 用
computed表示"派生数据",不要在里面做副作用。 - 如果需要传参,优先用工厂函数 + ref 参数,当作 composable。
- 可写 computed(getter/setter)是实现组件内部 v-model 的标准方式。
- 不要把异步逻辑放在 computed;使用
watch/watchEffect处理异步或副作用。 - 小而清晰的 computed 更容易组合、测试与复用。
- 注意
reactive解构导致的响应性丢失,使用toRefs保持响应性。 - 在 SSR 场景下保证 computed 的确定性,尽量不要依赖客户端专有 API。
18. 快速参考 Cheat Sheet
- 生成只读 computed:
js
const x = computed(() => a.value + b.value)
- 生成可写 computed:
js
const y = computed({
get: () => foo.value,
set: v => foo.value = v
})
- 工厂式 computed(传参):
js
function useFiltered(listRef, qRef) {
return computed(() => listRef.value.filter(x => x.includes(qRef.value)))
}
- computed 返回函数(运行时参数):
js
const getterFunc = computed(() => (p) => expensiveCalc(p, base.value))
- 避免:在 computed 中做网络请求、console side-effects、setTimeout 等耗时操作。