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

相关推荐
玄尺_0072 小时前
uniapp h5端使浏览器弹出下载框
前端·javascript·uni-app
军军君012 小时前
Three.js基础功能学习三:纹理与光照
前端·javascript·3d·前端框架·three·三维·三维框架
淡笑沐白2 小时前
Vue3基础语法教程
前端·javascript·vue.js
一 乐2 小时前
景区管理|基于springboot + vue景区管理系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·学习
Sylus_sui2 小时前
企业级Axios封装实战指南
vue.js
幽络源小助理2 小时前
SpringBoot+Vue大型商场应急预案管理系统源码 | Java安全类项目免费下载 – 幽络源
java·vue.js·spring boot
JIngJaneIL2 小时前
基于java + vue连锁门店管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot
Mintopia2 小时前
🧠 从零开始:纯手写一个支持流式 JSON 解析的 React Renderer
前端·数据结构·react.js
可触的未来,发芽的智生2 小时前
2025年终总结:智能涌现的思考→放弃冯诺依曼架构范式,拥抱“约束产生智能”
javascript·人工智能·python·神经网络·程序人生