本文是 Vue3 系列第四篇,将深入探讨计算属性这一重要概念。计算属性就像是 Vue 应用中的"智能助手",它能够根据其他数据自动计算得出新的数据,并且具有高效的缓存机制。理解计算属性的工作原理和使用场景,能够让我们编写出更加高效和可维护的 Vue 应用。
一、为什么需要计算属性?
在日常开发中,我们经常会遇到这样的场景:需要根据已有的数据计算出一些衍生数据。比如,根据商品单价和数量计算总价,根据用户信息组合出完整的姓名,或者根据列表数据筛选出符合条件的内容。
你可能会想到在模板中直接写表达式,比如 {``{ price * quantity }},或者使用方法来计算 {``{ calculateTotal() }}。但这些方式都有各自的局限性:
-
模板表达式:当逻辑复杂时,模板会变得臃肿难读
-
方法调用:每次重新渲染都会执行,性能较差
这时候,计算属性就派上用场了。计算属性就像是给数据加了一个"智能处理器",它能够自动追踪依赖,只有当依赖的数据发生变化时才会重新计算,其他时候直接使用缓存的结果。
想象一下,你有一个复杂的数学公式,你不需要每次都手动计算,而是有一个智能助手帮你记住结果,只有当公式中的变量发生变化时,它才会重新计算。这就是计算属性的作用。
二、计算属性的基本用法
让我们通过一个简单的例子来理解计算属性的基本用法:
html
<template>
<div>
<p>商品单价: {{ price }} 元</p>
<p>购买数量: {{ quantity }}</p>
<p>总价: {{ totalPrice }} 元</p>
<button @click="increaseQuantity">增加数量</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const price = ref(100)
const quantity = ref(2)
// 计算属性 - 自动计算总价
const totalPrice = computed(() => {
console.log('计算属性被重新计算了')
return price.value * quantity.value
})
const increaseQuantity = () => {
quantity.value++
}
</script>
代码详细解释:
这段代码展示了一个经典的计算属性使用场景。我们有一个商品单价 price 和购买数量 quantity,需要计算出总价 totalPrice。
关键点分析:
-
导入 computed :首先从 Vue 中导入
computed函数 -
定义计算属性 :
totalPrice是一个计算属性,它通过computed(() => { ... })来定义 -
自动追踪依赖 :计算属性内部使用了
price.value和quantity.value,Vue 会自动追踪这些依赖 -
缓存机制 :只有在
price或quantity变化时,计算属性才会重新执行 -
响应式更新 :当点击按钮增加数量时,
totalPrice会自动更新
你可以尝试多次点击"增加数量"按钮,会发现控制台只在第一次和数量变化时打印"计算属性被重新计算了",这证明了计算属性的缓存特性。
三、计算属性的缓存优势
为了更清楚地展示计算属性的缓存优势,让我们对比一下计算属性和方法的区别:
html
<template>
<div>
<h3>计算属性 vs 方法</h3>
<p>计算属性结果: {{ computedResult }} </p>
<p>计算属性结果: {{ computedResult }} </p>
<p>计算属性结果: {{ computedResult }} </p>
<p>方法结果: {{ methodResult() }} </p>
<p>方法结果: {{ methodResult() }} </p>
<p>方法结果: {{ methodResult() }} </p>
<button @click="updateData">更新数据</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const data = ref(1)
// 计算属性 - 有缓存
const computedResult = computed(() => {
console.log('计算属性执行了')
return data.value * 2
})
// 方法 - 无缓存
const methodResult = () => {
console.log('方法执行了')
return data.value * 2
}
const updateData = () => {
data.value++
}
</script>
代码详细解释:
这个例子清晰地展示了计算属性和方法的区别。我们在模板中分别三次使用计算属性和三次调用方法。
运行结果分析:
当你运行这个示例时,会发现:
-
初始渲染时:计算属性只执行一次,而方法执行了三次
-
点击更新按钮时:计算属性执行一次,方法又执行了三次
缓存机制的原理:
计算属性的缓存机制就像是给计算结果拍了一张快照。只要依赖的数据没有变化,每次访问计算属性都会返回这张快照,而不会重新计算。只有当依赖的数据发生变化时,它才会重新拍照(重新计算)。
这种机制在处理复杂计算时特别有用。想象一下,如果你有一个需要大量计算的统计数据,使用计算属性可以避免不必要的重复计算,显著提升性能。
四、可读写的计算属性
默认情况下,计算属性是只读的,我们只能获取它的值,不能直接设置。但有时候,我们希望能够"反向"设置计算属性,让它自动更新依赖的数据。这时候就需要使用计算属性的完整写法。
只读计算属性的限制
html
<template>
<div>
<p>全名: {{ fullName }}</p>
<button @click="tryToChange">尝试修改计算属性</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 只读的计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
const tryToChange = () => {
// 这样会报错!
// fullName.value = '李 四'
}
</script>
问题说明:
默认的计算属性是只读的,如果你尝试直接给计算属性赋值,TypeScript 会报错,运行时也会出现警告。这是因为计算属性本身不存储值,它只是根据依赖数据计算得出的结果。
可读写计算属性的实现
html
<template>
<div>
<p>全名: {{ fullName }}</p>
<input v-model="fullName" placeholder="输入新全名" />
<p>姓氏: {{ firstName }}</p>
<p>名字: {{ lastName }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 可读写的计算属性
const fullName = computed({
// getter - 读取时调用
get() {
console.log('getter被调用')
return `${firstName.value} ${lastName.value}`
},
// setter - 设置时调用
set(newValue: string) {
console.log('setter被调用,新值:', newValue)
const names = newValue.split(' ')
if (names.length >= 2) {
firstName.value = names[0]
lastName.value = names[1]
}
}
})
</script>
代码详细解释:
这个例子展示了如何创建可读写的计算属性。我们使用 computed() 函数的完整形式,传入一个包含 get 和 set 方法的对象。
getter 和 setter 的工作机制:
-
getter 方法:
-
当在模板或 JavaScript 中读取
fullName时自动调用 -
返回由
firstName和lastName组合的全名 -
依赖追踪在这里发生
-
-
setter 方法:
-
当给
fullName赋值时自动调用 -
接收一个参数
newValue,即要设置的新值 -
在这里实现"反向更新"逻辑:将全名拆分为姓氏和名字
-
关键理解点:
重要的是要理解,计算属性本身并不存储数据。当我们"修改"计算属性时,实际上是在修改它依赖的原始数据。在上面的例子中,修改 fullName 实际上是在修改 firstName 和 lastName。
五、计算属性的高级用法
复杂数据处理的场景
html
<template>
<div>
<h3>商品筛选</h3>
<input v-model="searchKeyword" placeholder="搜索商品" />
<h4>符合条件的商品 ({{ filteredProducts.length }} 个)</h4>
<ul>
<li v-for="product in filteredProducts" :key="product.id">
{{ product.name }} - ¥{{ product.price }}
<span v-if="product.price > 100" class="expensive">(高价)</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface Product {
id: number
name: string
price: number
}
const searchKeyword = ref('')
const products = ref<Product[]>([
{ id: 1, name: '笔记本电脑', price: 5999 },
{ id: 2, name: '智能手机', price: 3999 },
{ id: 3, name: '平板电脑', price: 2999 },
{ id: 4, name: '无线耳机', price: 399 },
{ id: 5, name: '智能手表', price: 1299 }
])
// 复杂的计算属性 - 商品筛选
const filteredProducts = computed(() => {
const keyword = searchKeyword.value.toLowerCase().trim()
if (!keyword) {
return products.value
}
return products.value.filter(product =>
product.name.toLowerCase().includes(keyword)
)
})
</script>
<style>
.expensive {
color: #ff4444;
font-weight: bold;
}
</style>
码详细解释:
这个例子展示了计算属性在处理复杂数据场景下的威力。我们有一个商品列表,需要根据搜索关键词进行筛选。
计算属性的优势体现:
-
逻辑封装:将复杂的筛选逻辑封装在计算属性中,模板保持简洁
-
自动更新 :当
searchKeyword或products变化时,筛选结果自动更新 -
性能优化:只有在搜索关键词或商品列表变化时才重新计算
-
响应式:筛选结果的数量会自动显示在界面上
计算属性的组合使用
html
<template>
<div>
<h3>购物车统计</h3>
<p>总数量: {{ totalQuantity }}</p>
<p>总价格: {{ totalPrice }} 元</p>
<p>平均单价: {{ averagePrice }} 元</p>
<p>最贵商品: {{ mostExpensiveProduct?.name }}</p>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
interface CartItem {
id: number
name: string
price: number
quantity: number
}
const cartItems = ref<CartItem[]>([
{ id: 1, name: '商品A', price: 100, quantity: 2 },
{ id: 2, name: '商品B', price: 200, quantity: 1 },
{ id: 3, name: '商品C', price: 50, quantity: 3 }
])
// 多个计算属性可以相互依赖
const totalQuantity = computed(() => {
return cartItems.value.reduce((sum, item) => sum + item.quantity, 0)
})
const totalPrice = computed(() => {
return cartItems.value.reduce((sum, item) =>
sum + (item.price * item.quantity), 0
)
})
const averagePrice = computed(() => {
if (totalQuantity.value === 0) return 0
return totalPrice.value / totalQuantity.value
})
const mostExpensiveProduct = computed(() => {
if (cartItems.value.length === 0) return null
return cartItems.value.reduce((max, item) =>
item.price > max.price ? item : max
)
})
</script>
代码详细解释:
这个例子展示了如何组合使用多个计算属性。每个计算属性负责一个特定的统计计算,有些计算属性还依赖于其他计算属性。
计算属性组合的优势:
-
职责分离:每个计算属性只负责一个具体的计算任务
-
依赖管理:Vue 自动管理计算属性之间的依赖关系
-
可维护性:代码结构清晰,易于理解和修改
-
复用性:可以在多个地方使用同一个计算属性
六、计算属性的最佳实践
1. 避免副作用
计算属性应该是纯函数,不应该有副作用:
TypeScript
// 好的做法 - 纯函数
const total = computed(() => prices.value.reduce((a, b) => a + b, 0))
// 不好的做法 - 有副作用
const total = computed(() => {
console.log('计算中...') // 副作用:日志输出
sendAnalytics() // 副作用:发送分析数据
return prices.value.reduce((a, b) => a + b, 0)
})
2. 避免直接修改依赖
计算属性应该只依赖于其他响应式数据,而不应该修改它们:
TypeScript
// 好的做法 - 只读依赖
const discountedPrice = computed(() => price.value * 0.9)
// 不好的做法 - 修改依赖
const discountedPrice = computed(() => {
price.value = price.value * 0.9 // 错误:修改了依赖
return price.value
})
3. 合理使用计算属性缓存
充分利用计算属性的缓存特性:
TypeScript
// 复杂的计算适合用计算属性
const complexResult = computed(() => {
// 假设这是很复杂的计算
return heavyCalculation(data.value)
})
// 简单的表达式可以直接在模板中写
// <p>{{ a + b }}</p> // 这样更简单直接
七、总结
通过本文的学习,相信你已经对 Vue3 的计算属性有了全面的理解。
核心要点回顾
计算属性是 Vue 中非常重要的响应式特性,它能够根据依赖数据自动计算得出新的数据,并且具有智能的缓存机制。计算属性可以是只读的,也可以通过 getter/setter 实现可读写。合理使用计算属性能够显著提升应用性能和代码可维护性。
计算属性的核心优势
-
自动缓存:依赖不变时不重新计算,提升性能
-
响应式更新:依赖变化时自动更新
-
代码组织:将复杂逻辑从模板中抽离
-
类型安全:在 TypeScript 中具有良好的类型推断
下一节我们将一起探讨watch和watchEffect监视器。
关于 Vue3 计算属性有任何疑问?欢迎在评论区提出,我们会详细解答!