# 深度解析 Vue 3 Computed 计算属性:从响应式原理到高阶实战
在 Vue 3 开发中,`computed`(计算属性)是我们最频繁使用的 API 之一。然而,很多开发者仅停留在“用来拼接字符串”或“做简单数值计算”的基础用法上,对其背后的**缓存机制、惰性求值原理以及高阶可写用法**缺乏深层次的理解。
本文将从**核心本质、应用场景、高阶用法及避坑指南**全方位拆解 `computed`。
---
## 一、 核心本质:如何理解 Computed?
理解 `computed` 的关键在于一句话:**“它是基于响应式依赖进行缓存的派生状态。”**
拆解来看包含三个核心特征:
1. **派生性(Derived State)**:计算属性本身不直接存储独立数据,它的值是由其他响应式数据(`ref` / `reactive` / `props`)计算得出的。
2. **缓存性(Cached)**:这是它与普通 `methods` 函数的核心区别!只要依赖的响应式数据未发生改变,多次访问 `computed` 只会**直接返回上一次缓存的结果**,避免重复计算开销。
3. **惰性求值(Lazy Evaluation)**:当依赖的响应式数据改变时,`computed` **不会立刻重新计算**,而是仅将内部的“脏标记(dirty)”设为 `true`。直到下一次**视图渲染或代码显式读取**它时,才会真正触发重算并刷新缓存。
---
## 二、 基础与常见应用场景
### 1. 数据格式化与拼装
```vue
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 依靠缓存避免字符串重复拼接
const fullName = computed(() => `${firstName.value}${lastName.value}`)
</script>
2. 列表筛选与动态过滤
在不污染/修改原始列表的前提下,根据搜素关键字生成派生列表:
<script setup lang="ts">
import { ref, computed } from 'vue'
const searchKey = ref('')
const goodsList = ref([
{ id: 1, name: 'Apple iPhone' },
{ id: 2, name: 'Huawei Mate' },
{ id: 3, name: 'Xiaomi Phone' }
])
const filteredList = computed(() => {
if (!searchKey.value) return goodsList.value
return goodsList.value.filter(item =>
item.name.toLowerCase().includes(searchKey.value.toLowerCase())
)
})
</script>
3. 多依赖聚合统计(如购物车)
<script setup lang="ts">
import { ref, computed } from 'vue'
const cartItems = ref([
{ price: 100, count: 2, selected: true },
{ price: 200, count: 1, selected: false }
])
// 聚合计算总价与全选状态
const totalPrice = computed(() => {
return cartItems.value
.filter(item => item.selected)
.reduce((sum, item) => sum + item.price * item.count, 0)
})
</script>
三、 高阶用法与进阶技巧
1. 可写计算属性(Writable Computed:get 与 set)
默认的 computed 是只读的。但在需要双向绑定(如 v-model)或改变计算属性需反向拆分/同步更新原始数据源时,可以使用带有 get 和 set 的对象形式。
示例 A:表单双向绑定与反向拆分
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue: string) {
// 收到赋值时,反向更新原始数据源
const [first, last] = newValue.split(' ')
firstName.value = first || ''
lastName.value = last || ''
}
})
</script>
<template>
<input v-model="fullName" />
</template>
示例 B:组件层层传递中 v-model 的二次封装代理
在封装 UI 组件时,利用 computed 的 set 转发 emit,优雅实现属性中转:
<!-- CustomInput.vue -->
<script setup lang="ts">
import { computed } from 'vue'
const props = defineProps<{ modelValue: string }>()
const emit = defineEmits(['update:modelValue'])
const value = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
</script>
<template>
<el-input v-model="value" />
</template>
2. 计算属性闭包传参
computed 本身不能直接接收参数。若在 v-for 循环中需要根据每项数据做计算,可以通过让 computed 返回一个闭包函数来实现:
<script setup lang="ts">
import { ref, computed } from 'vue'
const userRoles = ref(['editor', 'author'])
// 返回一个闭包函数
const hasPermission = computed(() => {
return (role: string) => userRoles.value.includes(role)
})
</script>
<template>
<button v-if="hasPermission('editor')">编辑文章</button>
<button v-if="hasPermission('admin')">删除文章</button>
</template>
⚠️ 注意:返回闭包函数时,内部函数在模板渲染调用时无法享受缓存,但外层仍能保留对依赖的追踪。
3. 计算属性调试(Debugging)
Vue 3 支持为 computed 传入选项参数,方便追踪依赖收集(onTrack)与更新触发(onTrigger):
const count = ref(0)
const double = computed(
() => count.value * 2,
{
onTrack(e) {
console.log('依赖追踪:', e.target)
},
onTrigger(e) {
console.log('依赖更新:', e.target)
}
}
)
四、 避坑指南与最佳实践
1. 绝对不要在 get 中产生副作用(Side Effects)
computed 的 get 函数必须是纯函数(Pure Function)。
- ❌ 严禁:在
get内部修改其他响应式数据、发起 AJAX 异步请求或直接操作 DOM。这可能导致响应式追踪引发无限死循环。 - ✅ 建议:任何异步操作或主动副作用,请使用
watch或watchEffect。
2. 避免在 set 中编写无关的隐蔽逻辑
在 set 中修改数据源是完全合法的,但修改的目标应该与该 computed 在业务上有明确的因果/连带关系(如标志位更新、原始字段拆分),避免触发不相干模块的隐蔽变更。
3. 区分 computed 与 watch
computed:用于根据旧数据计算导出新数据(同步、有返回值、有缓存)。watch:用于监听数据变化并执行操作(可处理异步、无返回值、用于副作用处理)。
💡 总结思考决策树
在编写计算属性时,可以按照以下思路进行思考决策:
需要根据现有数据导出新状态?
└── 否:使用普通变量 / 函数 / watch
└── 是:使用 computed
└── 是否频繁变化且多处使用? ➔ 享受缓存收益
└── 是否需要配合 v-model / 逆向改源数据?
├── 否(90% 场景):使用只读函数签名 computed(() => ...)
└── 是(10% 场景):使用开箱 get/set 对象签名 computed({ get, set })