Vue 3 计算属性完全指南
本文档从零开始讲解 Vue 3 的计算属性(Computed):是什么、为什么用、和方法的区别、只读与可写、常见场景和易错点,配有大量示例,适合新手系统学习。
一、什么是计算属性?
1.1 生活中的类比
有时你需要根据已有数据“算出来”一个新结果,例如:
- 根据姓和名得到“全名”
- 根据购物车列表得到“总价”
- 根据列表和筛选条件得到“筛选后的列表”
在 Vue 里,这种由其它响应式数据推导出来的值,可以用 计算属性 来表示。
你只要写好“怎么算”,Vue 会自动在依赖变化时重新计算,并像普通数据一样在模板里使用。
1.2 计算属性长什么样?
计算属性在 <script setup> 里用 computed() 定义:
你传入一个函数,这个函数返回一个值,这个值就是“计算出来的结果”。
在模板里使用时,和 ref 一样不用写 .value;在 script 里要用 .value 才能拿到值。
<template>
<div>
<p>全名:{{ fullName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed(() => {
return firstName.value + lastName.value
})
</script>
- fullName 是计算属性:由
firstName和lastName推导出来的。 - 当
firstName或lastName变化时,fullName会自动重新计算。 - 模板里直接写
{{ fullName }},不需要fullName.value。
二、为什么用计算属性?和“方法”有什么区别?
2.1 用方法也能“算”出来
下面用方法实现“全名”:
<template>
<div>
<p>全名:{{ getFullName() }}</p>
</div>
</template>
<script setup>
import { ref } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
function getFullName() {
return firstName.value + lastName.value
}
</script>
效果看起来一样,但有一个重要区别:方法每次渲染都会重新执行,而计算属性会缓存结果。
2.2 计算属性会“缓存”,方法不会
- 计算属性:只有它依赖的数据(如上面的
firstName、lastName)变化时,才会重新计算;否则一直用上一次的结果(缓存)。 - 方法:每次模板重新渲染(例如其它数据变了),都会再执行一遍。
所以:
- 需要根据某些数据推导出一个值,并且希望依赖没变就不重复算 → 用 computed。
- 需要执行一段逻辑(比如点击时调用)、不强调“缓存一个结果” → 用 方法。
2.3 简单对比表
| 对比项 | 计算属性 computed | 方法 methods |
|---|---|---|
| 写法 | computed(() => ...) |
function fn() { } |
| 模板里使用 | {{ fullName }} |
{{ getFullName() }} |
| 是否缓存 | 是,依赖不变不重算 | 否,每次渲染都可能执行 |
| 适用 | 派生数据、过滤、合计 | 事件处理、不依赖“缓存”的逻辑 |
三、基本语法
3.1 只读计算属性(最常用)
语法: const 变量 = computed(() => { return 某个值 })
- 传入一个函数(getter),返回值就是计算属性的值。
- 这个函数里用到的 ref/reactive 会被 Vue 自动收集为“依赖”;依赖变了,计算属性会重新算。
<template>
<div>
<p>单价:{{ price }},数量:{{ count }}</p>
<p>总价:{{ total }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const price = ref(10)
const count = ref(3)
const total = computed(() => {
return price.value * count.value
})
</script>
- total 依赖
price和count;任一个变化,total会重新计算。 - 在 script 里若要用
total的值,要写 total.value;在 template 里直接写 total 即可。
3.2 计算属性在 script 里要写 .value
计算属性返回的也是一个 ref(只读),所以在 <script setup> 里读取时要 .value:
<script setup>
import { ref, computed } from 'vue'
const count = ref(2)
const double = computed(() => count.value * 2)
console.log(double.value) // 4
function printDouble() {
alert(double.value)
}
</script>
模板里不需要:
<template>
<p>{{ double }}</p>
</template>
四、可写计算属性(getter + setter)
默认情况下,计算属性是只读的:你不能写 fullName.value = '李四'。
若你希望“从外面给计算属性赋值时,反过来更新它依赖的源数据”,可以写成 get + set 的形式。
4.1 语法
传入一个对象,包含 get 和 set 两个函数:
const 变量 = computed({
get() {
return 根据其它数据算出的值
},
set(newValue) {
// 根据 newValue 去更新依赖的源数据
}
})
4.2 示例:全名可读可写
“全名”由“姓”和“名”组成;当用户直接改“全名”时,自动拆成“姓”和“名”写回去。
<template>
<div>
<p>姓:<input v-model="firstName" /></p>
<p>名:<input v-model="lastName" /></p>
<p>全名:<input v-model="fullName" /></p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get() {
return firstName.value + lastName.value
},
set(newValue) {
const len = newValue.length
if (len >= 2) {
firstName.value = newValue[0]
lastName.value = newValue.slice(1)
} else if (len === 1) {
firstName.value = newValue
lastName.value = ''
} else {
firstName.value = ''
lastName.value = ''
}
}
})
</script>
- get:读
fullName时,返回firstName + lastName。 - set:写
fullName时(例如在 input 里改全名),把新值拆成姓和名,更新firstName、lastName。
可写计算属性用得相对少,了解即可;大部分场景只读就够用。
五、常见使用场景
5.1 由多个字段组合成一个显示值
<template>
<p>完整地址:{{ fullAddress }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const province = ref('广东省')
const city = ref('深圳市')
const district = ref('南山区')
const fullAddress = computed(() => {
return province.value + city.value + district.value
})
</script>
5.2 列表过滤(根据关键词筛选)
<template>
<div>
<input v-model="keyword" placeholder="搜索" />
<ul>
<li v-for="item in filteredList" :key="item.id">{{ item.name }}</li>
</ul>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const keyword = ref('')
const list = ref([
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橙子' }
])
const filteredList = computed(() => {
const k = keyword.value.trim().toLowerCase()
if (!k) return list.value
return list.value.filter((item) => item.name.toLowerCase().includes(k))
})
</script>
- filteredList 依赖
keyword和list;任一变化都会重新过滤。 - 模板里用
v-for="item in filteredList",无需在模板里写过滤逻辑,也更利于配合v-for的:key。
5.3 列表排序(不改变原数组)
<template>
<ul>
<li v-for="item in sortedList" :key="item.id">
{{ item.name }} - {{ item.score }} 分
</li>
</ul>
</template>
<script setup>
import { ref, computed } from 'vue'
const list = ref([
{ id: 1, name: '小明', score: 85 },
{ id: 2, name: '小红', score: 92 },
{ id: 3, name: '小刚', score: 78 }
])
const sortedList = computed(() => {
return [...list.value].sort((a, b) => b.score - a.score)
})
</script>
用 [...list.value] 先拷贝再排序,不修改原数组,符合“计算属性不产生副作用”的习惯。
5.4 合计、统计
<template>
<div>
<p>总价:¥{{ totalPrice }}</p>
<p>数量:{{ totalCount }} 件</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const cart = ref([
{ id: 1, name: '苹果', price: 5, count: 2 },
{ id: 2, name: '香蕉', price: 3, count: 5 }
])
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
const totalCount = computed(() => {
return cart.value.reduce((sum, item) => sum + item.count, 0)
})
</script>
5.5 布尔类“是否……”判断
<template>
<div>
<button :disabled="!canSubmit">提交</button>
<p v-if="isEmpty">列表为空</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const list = ref([])
const form = ref({ name: '', age: 0 })
const isEmpty = computed(() => list.value.length === 0)
const canSubmit = computed(() => {
return form.value.name.trim() !== '' && form.value.age > 0
})
</script>
5.6 格式化显示(日期、金额等)
<template>
<p>下单时间:{{ formattedDate }}</p>
<p>金额:{{ formattedPrice }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const orderDate = ref('2025-02-24')
const price = ref(1234.5)
const formattedDate = computed(() => {
return orderDate.value.replace(/-/g, '/')
})
const formattedPrice = computed(() => {
return '¥' + price.value.toFixed(2)
})
</script>
复杂格式化可以再拆成方法或工具函数,在 computed 里调用即可。
5.7 计算属性依赖另一个计算属性
计算属性可以依赖别的 ref 或计算属性,形成“链式推导”:
<template>
<p>总价(含税):{{ totalWithTax }}</p>
</template>
<script setup>
import { ref, computed } from 'vue'
const cart = ref([
{ price: 10, count: 2 },
{ price: 5, count: 3 }
])
const total = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.count, 0)
})
const taxRate = ref(0.1)
const totalWithTax = computed(() => {
return (total.value * (1 + taxRate.value)).toFixed(2)
})
</script>
- totalWithTax 依赖 total 和 taxRate;total 本身是计算属性,依赖 cart。
- 只要 cart 或 taxRate 变化,整条链都会按需重新计算。
六、注意点与易错点
6.1 不要在计算属性里改其它数据(避免副作用)
计算属性应该是纯函数:根据依赖算出结果,不要在里面写 赋值、请求接口、DOM 操作 等。
否则依赖关系会乱,缓存和更新时机也可能不符合预期。
// 不推荐
const bad = computed(() => {
count.value++ // 不要在里面改别的数据
return count.value
})
需要“算完再干别的事”,放在 watch 或 事件处理函数 里更合适。
6.2 计算属性要“返回”一个值
getter 函数必须 return 一个值,这个值才会成为计算属性的结果。
若忘记 return,计算属性会是 undefined。
const wrong = computed(() => {
const x = a.value + b.value
// 忘记 return,wrong 一直是 undefined
})
const right = computed(() => {
return a.value + b.value
})
6.3 依赖要在“读取时”被访问到
Vue 通过在运行 getter 时记录用到了哪些 ref/reactive 来收集依赖。
若你把依赖写在条件里,某次执行没走到,那一次就不会被当作依赖,可能导致“该更新时没更新”:
// 若 condition 为 false,someRef 可能不会被收集为依赖
const risky = computed(() => {
if (condition.value) {
return someRef.value
}
return 0
})
尽量让计算属性用到的响应式数据在每次 getter 执行时都能被访问到(至少在同一逻辑分支里稳定出现)。
6.4 不要给只读计算属性赋值
没有写 set 的计算属性是只读的,不能 xxx.value = 新值,否则会报错。
需要“可写”时,用 get/set 形式的 computed。
七、计算属性与 ref / reactive 对比
| 对比项 | 计算属性 computed | ref / reactive |
|---|---|---|
| 来源 | 由其它数据推导 | 直接定义的源数据 |
| 是否可写 | 默认只读,可配 set | 可写 |
| 是否缓存 | 是 | 不涉及“重算” |
| 典型用途 | 派生状态、过滤、合计 | 原始状态、用户输入 |
简单记:“算出来的”用 computed,“直接存的”用 ref/reactive。
八、速查表与学习建议
8.1 速查表
| 需求 | 写法 |
|---|---|
| 只读计算属性 | const x = computed(() => 返回值) |
| 可写计算属性 | computed({ get() { return ... }, set(v) { ... } }) |
| 模板里使用 | 直接 {{ x }},不用 .value |
| script 里使用 | x.value |
| 依赖 ref/reactive | 在 getter 里正常 .value 或访问属性即可,Vue 自动收集依赖 |
8.2 学习建议
- 先掌握只读的
computed(() => ...),能根据列表、表单等算出“过滤结果、总价、是否可提交”等。 - 理解缓存:依赖不变就不重算,适合派生数据。
- 不在计算属性里写副作用;需要“算完再请求/再改别的”时用 watch 或方法。
- 可写计算属性(get/set)在需要“双向派生”时再用(如全名 ↔ 姓+名)。
把本文档里的示例在项目里敲一遍、改依赖数据看结果是否自动更新,会掌握得更牢。祝你学习顺利。