大家好,我是加洛斯。作为一名程序员👨💻,我深深信奉费曼学习法——教,是最好的学 📚。这里是我的知识笔记与分享,旨在把复杂的东西讲明白。如果发现有误🔍,万分欢迎你帮我指出来!废话不多说,正文开始 👇
一、基础知识
1.1 什么是计算属性?
Vue中的计算属性是一个非常重要的响应式特性,它允许你声明式地定义依赖于其他响应式数据的值。当依赖的数据发生变化时,计算属性会自动重新计算。
1.2 为什么要使用计算属性?
// 没有计算属性时,我们需要这样做
<template>
<div>
<!-- 每次都要这样拼接 -->
<p>用户:{{ firstName + ' ' + lastName }}</p>
<p>欢迎语:{{ '欢迎 ' + firstName + ' ' + lastName + ' 登录!' }}</p>
</div>
</template>
<script setup>
const firstName = '张'
const lastName = '三'
</script>
那么这么写就带来了问题:
- 同样的逻辑
firstName + ' ' + lastName写了多次 - 如果修改逻辑(比如中间加逗号),要改很多地方
- 每次渲染都要重新计算
综上,如果定义一个变量计算firstName + ' ' + lastName 的值,那么只需要在模版位置写这一个变量就行、如果要修改的话也只需要修改一个地方,那这个变量所代表的就是今天我们要了解与学习的计算属性。
1.3 computed函数
computed是Vue3中的一个核心函数,用于创建响应式计算值。它的基础语法很简单,让我们用上面的代码举个例子。
// 创建计算属性:就像定义一个变量,但是它是“自动计算”的
const fullName = computed(function(){
// 这里返回的就是 fullName 的值
return firstName.value + ' ' + lastName.value
})
//这里运用了ES6箭头函数的写法,也是现在最推荐的写法
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
// 还可以利用箭头函数的隐式返回特性简写
const fullName = computed(() => firstName.value + ' ' + lastName.value)
箭头函数隐式返回特性:箭头函数有一种简洁语法:当函数体只有单个表达式时,可以省略大括号
{}和return关键字,表达式的结果会自动返回。
<template>
<div>
<p>用户:{{ fullName }}</p>
<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>
上面的代码就是我们调整后的写法,这样是不是就整洁多了?但这还不是最优的版本!
1.4 计算属性的值也可以被其他计算属性依赖
我们看如下代码,fullName作为计算属性,而他的值也可以被别的计算属性所依赖。
<template>
<div>
<p>用户:{{ fullName }}</p>
<p>欢迎语:{{ welcomeMessage }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed(() => {
return firstName.value + ' ' + lastName.value
})
const welcomeMessage = computed(() => {
return '欢迎 ' + fullName.value + ' 登录!'
})
</script>
二、缓存
缓存是指计算属性会记住它的计算结果,当依赖的响应式数据没有变化时,直接返回之前计算的结果,而不会重新执行计算函数。
计算属性内部维护一个依赖列表和一个脏检查标记:
class ComputedRef {
constructor(getter) {
this._value = undefined
this._dirty = true // 脏标记:true表示需要重新计算
this._deps = new Set() // 依赖收集
this._getter = getter
}
get value() {
// 如果依赖没有变化(dirty=false),直接返回缓存值
if (!this._dirty) {
return this._value
}
// 否则重新计算
this._value = this._getter()
this._dirty = false // 标记为已计算
return this._value
}
// 当依赖变化时,标记为脏
markDirty() {
this._dirty = true
}
}
我们来看看缓存效果是什么样式的,可以看到,只有当依赖的数据发生变化后才会重新计算,否则只会重新计算一次。
import { ref, computed } from 'vue'
const count = ref(0)
let computeCount = 0
// 计算属性
const doubled = computed(() => {
computeCount++ // 计数器,查看计算了几次
console.log('计算属性执行了!')
return count.value * 2
})
// 测试
console.log('第一次访问:', doubled.value) // 输出:"计算属性执行了!"
console.log('计算次数:', computeCount) // 1
console.log('第二次访问:', doubled.value) // 没有输出!
console.log('计算次数:', computeCount) // 仍然是1!
console.log('第三次访问:', doubled.value) // 没有输出!
console.log('计算次数:', computeCount) // 仍然是1!
// 只有当依赖变化时,才会重新计算
count.value = 1
console.log('依赖变化后第一次访问:', doubled.value) // 输出:"计算属性执行了!"
console.log('计算次数:', computeCount) // 2
三、实际应用场景
3.1 购物车计算
<template>
<div>
<h3>购物车</h3>
<div v-for="item in cart" :key="item.id">
{{ item.name }} - ¥{{ item.price }} × {{ item.quantity }}
</div>
<!-- 使用计算属性展示计算结果 -->
<p>总数量:{{ totalQuantity }} 件</p>
<p>总金额:¥{{ totalPrice }}</p>
<p v-if="hasDiscount">折扣后:¥{{ discountPrice }} (已打9折)</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
// 购物车数据
const cart = ref([
{ id: 1, name: '苹果', price: 5, quantity: 2 },
{ id: 2, name: '香蕉', price: 3, quantity: 3 },
{ id: 3, name: '橙子', price: 4, quantity: 1 }
])
// 计算总数量
const totalQuantity = computed(() => {
// 累加所有商品的购买数量
let sum = 0
cart.value.forEach(item => {
sum += item.quantity
})
return sum
})
// 计算总价格
const totalPrice = computed(() => {
let sum = 0
cart.value.forEach(item => {
sum += item.price * item.quantity
})
return sum
})
// 判断是否有折扣(满20元打9折)
const hasDiscount = computed(() => {
return totalPrice.value >= 20
})
// 计算折扣价
const discountPrice = computed(() => {
if (hasDiscount.value) {
return totalPrice.value * 0.9
}
return totalPrice.value
})
</script>
3.2 可写的计算属性
有时候我们需要既能读取又能设置计算属性:
<template>
<div>
<p>全名:{{ fullName }}</p>
<input v-model="fullName" placeholder="输入新名字">
<p>名字已被修改:{{ firstName }} {{ lastName }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 可写的计算属性
const fullName = computed({
// getter:读取时调用
get() {
return firstName.value + ' ' + lastName.value
},
// setter:设置时调用
set(newValue) {
// 当设置 fullName 时,拆分成 firstName 和 lastName
const names = newValue.split(' ')
if (names.length >= 2) {
firstName.value = names[0]
lastName.value = names[1]
}
}
})
// 现在可以这样设置
fullName.value = '李 四' // 会自动更新 firstName 和 lastName
</script>
四、常见问题
4.1 ❌ 错误:在计算属性中修改数据
这会导致无限循环或缓存被破坏进而影响性能。
// 错误示范
const badComputed = computed(() => {
// 不要在计算属性里修改数据!
someValue.value = 100 // ❌ 错误!
return something.value
})
4.2 ❌ 错误:依赖不响应式的数据
const notReactive = '我是普通字符串'
const computedValue = computed(() => {
return notReactive + '!' // ❌ 不会更新,因为 notReactive 不是响应式的
})
// 应该用 ref 或 reactive
const reactiveValue = ref('我是响应式字符串')
4.3 ❌ 错误:计算属性传递参数
计算属性本身不支持传参,因为它被设计为响应式依赖的派生值,而不是一个函数。
// 这样是行不通的!
const computedWithParam = computed((param) => {
return someValue.value + param
})
// 错误:computed() 只接受 getter 函数或对象,不支持额外参数
五、总结
5.1 计算属性的三大特点:
- 自动追踪依赖 - 依赖的数据变化时,自动重新计算
- 缓存结果 - 相同依赖时直接返回缓存,不重复计算
- 响应式更新 - 计算属性本身也是响应式的
5.2 什么时候用计算属性?
- ✅ 需要基于其他数据计算新值时
- ✅ 同样的计算逻辑在多个地方使用时
- ✅ 计算逻辑比较复杂,不想写在模板里时
- ✅ 需要缓存计算结果提高性能时
5.3 什么时候用普通方法?
- ✅ 需要每次都重新计算时
- ✅ 需要传递参数时(计算属性不能传参)
- ✅ 执行有副作用的操作时