computed 计算属性
computed()接受一个 getter 函数,返回一个只读的响应式 ref。计算属性会缓存结果,只有依赖的响应式数据变化时才重新计算——这是它与普通方法的核心区别。
速查卡片
| 项 | 内容 |
|---|---|
| 签名 | computed(getter) 或 computed({ get, set }) |
| 入参 | getter 函数 (oldValue?) => T,或 { get, set } 对象 |
| 返回值 | 只读模式:Readonly<Ref<T>>;可写模式:Ref<T> |
| 缓存 | ✅ 依赖不变不重新计算 |
| 懒执行 | ✅ 首次访问时才计算(未被访问永不执行) |
| 可写 | ✅ 提供 get + set 即可 |
| 调试 | ✅ onTrack / onTrigger(3.2+) |
| 版本 | ≥ 3.0.0 |
| 场景 | 推荐 |
|---|---|
| 从已有响应式数据派生新值 | computed |
| 监听数据变化并执行副作用(API 调用、DOM 操作) | watch / watchEffect |
| 简单表达式 | 模板内直接写 |
| 需要 DOM 更新后执行 | watch + nextTick 或 watchEffect({ flush: 'post' }) |
概述
在模板中写复杂表达式会让模板臃肿难维护。计算属性将逻辑抽离到纯函数中,模板只负责展示:
<script setup lang="ts">
import { computed, ref } from 'vue'
const items = ref([1, 2, 3, 4, 5])
const evenItems = computed(() => items.value.filter(i => i % 2 === 0))
</script>
<template>
<ul>
<li v-for="item in evenItems" :key="item">{{ item }}</li>
</ul>
</template>evenItems 只在 items 变化时重新计算,其余时间返回缓存值。
基础概念
缓存机制——computed vs method 的核心区别
<script setup lang="ts">
import { computed, ref } from 'vue'
const count = ref(0)
// 计算属性——有缓存
const computedNow = computed(() => {
console.log('computed 执行')
return Date.now()
})
// 普通方法——无缓存
function methodNow() {
console.log('method 执行')
return Date.now()
}
</script>
<template>
<p>computed: {{ computedNow }}</p> <!-- 只执行一次,后续复用缓存 -->
<p>computed: {{ computedNow }}</p> <!-- 同上,不重新计算 -->
<p>method: {{ methodNow() }}</p> <!-- 每次访问都执行 -->
<p>method: {{ methodNow() }}</p> <!-- 同上 -->
</template>计算属性的缓存依赖响应式追踪:只有其 getter 中实际读取过的响应式数据发生变化时,才标记缓存失效。如果依赖的数据没变,即使组件重新渲染,计算属性也不会重新计算。
懒计算
计算属性在首次被访问时才执行 getter。如果从未在模板或 JS 中读取,计算逻辑永不触发。这和 watchEffect 的立即执行完全不同。
基础用法
示例一:最简——派生布尔值
<script setup lang="ts">
import { ref, computed } from 'vue'
const count = ref(0)
const isEven = computed(() => count.value % 2 === 0)
</script>
<template>
<p>count: {{ count }}</p>
<p>{{ isEven ? '偶数' : '奇数' }}</p>
<button @click="count++">+1</button>
</template>示例二:典型——搜索过滤
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
id: number
name: string
}
const users = ref<User[]>([
{ id: 1, name: 'Alice' },
{ id: 2, name: 'Bob' },
{ id: 3, name: 'Charlie' },
])
const query = ref('')
const filteredUsers = computed(() =>
users.value.filter(u => u.name.toLowerCase().includes(query.value.toLowerCase()))
)
</script>
<template>
<input v-model="query" placeholder="搜索..." />
<ul>
<li v-for="u in filteredUsers" :key="u.id">{{ u.name }}</li>
</ul>
</template>示例三:进阶——可写计算属性(v-model 绑定)
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get: () => `${firstName.value} ${lastName.value}`,
set: (val: string) => {
const parts = val.split(' ')
firstName.value = parts[0] || ''
lastName.value = parts[1] || ''
},
})
</script>
<template>
<input v-model="fullName" placeholder="全名" />
<p>姓: {{ firstName }}</p>
<p>名: {{ lastName }}</p>
</template>可写 computed 常用于封装多个响应式数据为一个逻辑单元,外部通过 v-model 无缝操作。
示例四:冷门——调试模式(onTrack / onTrigger)
Vue 3.2 起支持在 computed 中注入调试钩子,追踪依赖的收集和触发过程:
<script setup lang="ts">
import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)
const sum = computed(() => a.value + b.value, {
onTrack(e) {
// 当 a.value 或 b.value 的依赖被首次追踪时触发
console.log('track', e)
},
onTrigger(e) {
// 当 a.value 或 b.value 变化导致重新计算时触发
console.log('trigger', e)
},
})
</script>调试钩子仅在开发模式生效。还可配合 computed 的 oldValue 参数做缓存穿透检测:
// 利用 oldValue 做自定义缓存判断(3.4+ 稳定性优化)
import { computed, ref } from 'vue'
const items = ref([{ id: 1, value: 10 }, { id: 2, value: 20 }])
const total = computed((oldTotal) => {
const newTotal = items.value.reduce((sum, i) => sum + i.value, 0)
// 值没变则返回旧对象引用,避免下游不必要的更新
return newTotal === oldTotal?.value ? oldTotal : newTotal
})API 参考
computed() — 只读模式
| 项 | 内容 |
|---|---|
| 签名 | function computed<T>(getter: (oldValue: T | undefined) => T, debuggerOptions?: DebuggerOptions): Readonly<Ref<Readonly<T>>> |
| 入参 | getter: 计算函数,接收上一次返回值(首次为 undefined) |
| 返回值 | Readonly<Ref<T>>,只能读 .value,不可写入 |
| 版本 | ≥ 3.0.0;oldValue 参数 ≥ 3.4.0;debuggerOptions ≥ 3.2.0 |
computed() — 可写模式
| 项 | 内容 |
|---|---|
| 签名 | function computed<T>(options: { get: (oldValue: T | undefined) => T; set: (value: T) => void }, debuggerOptions?: DebuggerOptions): Ref<T> |
| 入参 | get: 读取时的计算函数;set: 写入时的反向解析函数 |
| 返回值 | Ref<T>,可读写 .value |
| 版本 | ≥ 3.0.0 |
DebuggerOptions
| 属性 | 签名 | 说明 |
|---|---|---|
onTrack | (e: DebuggerEvent) => void | 响应式依赖被追踪时调用 |
onTrigger | (e: DebuggerEvent) => void | 依赖变化触发重新计算时调用 |
interface DebuggerEvent {
effect: ReactiveEffect
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
}类型定义
// 只读
function computed<T>(
getter: (oldValue: T | undefined) => T,
debuggerOptions?: DebuggerOptions
): Readonly<Ref<Readonly<T>>>
// 可写
function computed<T>(
options: {
get: (oldValue: T | undefined) => T
set: (value: T) => void
},
debuggerOptions?: DebuggerOptions
): Ref<T>
// 调试事件
interface DebuggerEvent {
effect: ReactiveEffect
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
}
interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}注意 TypeScript 不会自动推断 computed 的泛型,但通常可以从 getter 返回值推断。如果推断失败(如使用三元表达式),显式标注:
computed<string>(() => ...)。
computed vs watch 决策指南
| 需求 | computed | watch |
|---|---|---|
| 从已有数据派生新值 | ✅ | ❌ |
| 执行副作用(API 调用) | ❌ | ✅ |
| 自动追踪依赖 | ✅ | ✅(watchEffect) |
| 懒执行(首次不跑) | ✅ | ✅(watch 默认懒) |
| 可写(反向赋值) | ✅ | ❌ |
| 有返回值 | ✅ | ❌ |
| 访问旧值 | ✅(oldValue 参数) | ✅((newVal, oldVal)) |
| 异步操作 | ❌ | ✅ |
| DOM 更新后回调 | ❌ | ✅({ flush: 'post' }) |
经验法则:如果需要"产生一个值"就选 computed,需要"做一件事"就选 watch。
边界情况与注意事项
依赖未使用时不会缓存失效
const a = ref(1)
const b = ref(2)
const sum = computed(() => a.value + b.value)
// 修改 c 不会触发 sum 重算——因为 sum 的 getter 没读 c
// 修改 b 会触发——因为 getter 中读取了 b.value每次创建新对象会导致缓存"失效"
// ❌ 每次返回新数组引用,下游 effect 总会触发
const filtered = computed(() => items.value.filter(i => i.active))
// ✅ 3.4+ 优化:值浅比较相同则复用旧值
// 但如果 filter 结果不同,仍然触发(正确行为)3.4 起 Vue 对 computed 值做浅比较,值不变不触发副作用。但若结果始终是新对象引用(如 map 返回新数组),下游仍会触发。优化方案:
const filtered = computed((oldVal) => {
const newVal = items.value.filter(i => i.active)
// 手动深比较,相同时返回旧引用
return isEqual(newVal, oldVal) ? oldVal : newVal
})不要在 computed 中执行副作用
// ❌ computed 中发起请求——不会按预期触发
const data = computed(() => {
fetch('/api/data').then(r => r.json())
return null
})
// ✅ 用 watch 或 watchEffect
watchEffect(() => {
fetch('/api/data').then(r => response.value = r.json())
})非响应式依赖不会触发重算
let external = 1
const doubled = computed(() => external * 2)
external = 5
console.log(doubled.value) // 仍是 2!external 不是响应式的循环依赖
const a = ref(1)
const b = computed(() => a.value + 1)
// ❌ 不要这样做:a = computed(() => b.value + 1) 会导致无限循环常见误区
❌ computed 可以替代所有模板表达式 → 简单表达式(
a + b)直接写在模板更简洁,过度 computed 反而增加心智负担。❌ computed 会"主动推送"更新 → 不。它是 pull-based(拉取模式):仅在被访问时才计算。未被访问的 computed 永不执行。
❌ computed 值每次访问都重新计算 → 错。有缓存,依赖不变返回上一次的结果。
❌ 可以在 computed 中执行异步操作 → 不能。computed 必须是同步纯函数。异步逻辑用
watch。❌ computed 依赖会自动解包
.value→ 不。getter 中访问 ref 必须写.value,computed 本身不会自动解包依赖。❌ 可写 computed 很少用 → 可写 computed 在表单双向绑定场景非常有用,封装一组相关状态为一个逻辑单元。
❌ 修改 computed 依赖会立即触发重算 → 不。computed 是懒计算,依赖变化只标记缓存失效,下次读取时才真正重算。
版本信息
| 版本 | 变更 |
|---|---|
| 3.0.0 | computed() 首次发布(只读 + 可写模式) |
| 3.2.0 | 新增 onTrack / onTrigger 调试钩子 |
| 3.4.0 | Computed Stability:值浅比较不变时不触发副作用;oldValue 参数支持 |
来源
| 来源 | 链接 |
|---|---|
| 官方指南 — Computed Properties | vuejs.org/guide/essentials/computed |
| 官方 API — computed() | vuejs.org/api/reactivity-core#computed |
| 性能指南 — Computed Stability | vuejs.org/guide/best-practices/performance#computed-stability |
| 官方指南 — Computed Debugging | vuejs.org/guide/extras/reactivity-in-depth#computed-debugging |
最后整理:2026-07-05 · 对应 Vue 版本:3.5.x