Vue 3 setup 语法糖 computed 的深度使用

目录

  • [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 + watchwatchEffect)
      • [推荐做法 B --- 如果想"声明式"一点,可返回一个函数或封装成 composable:](#推荐做法 B — 如果想“声明式”一点,可返回一个函数或封装成 composable:)
  • [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(代码片段合集))
  • [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 返回一个 RefComputedRef),在 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 --- 工厂函数(推荐用于可复用、参数在创建时确定)

把参数变成 refreactive,传入 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 的缓存、惰性求值与依赖追踪(原理要点)

核心行为(简要版):

  1. computed 的 getter 惰性(lazy) :只有当读取 .value(或模板访问)时才会执行 getter。
  2. 缓存:第一次读取 getter 计算并缓存结果,后续读取只返回缓存,直到某个依赖发生变化(被触发),此时缓存被标记为"脏",下一次读取才会重新计算。
  3. 依赖被自动追踪 :在 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),会丢失响应性 。正确的做法是 toRefstoRef

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 + watchwatchEffect

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. 性能优化策略与常见陷阱

性能建议

  1. 把昂贵运算放进 computed,避免每次渲染重复计算。
  2. 拆分 computed :小而专一的 computed 更容易缓存和组合(例如 filteredsortedpaged 分开)。
  3. 避免在 getter 中做副作用或长时间阻塞(网络请求、同步 I/O 等)。
  4. 在大型列表上使用虚拟滚动(virtual scroll),computed 的结果即便缓存但渲染大量 DOM 仍然慢。
  5. 对于高频变化但 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 + watchwatchEffect

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 等耗时操作。

相关推荐
崔庆才丨静觅1 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60612 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了2 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅2 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅2 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅3 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment3 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅3 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊3 小时前
jwt介绍
前端
爱敲代码的小鱼3 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax