计算属性computed深度解析
在Vue 3中,计算属性是一个非常强大的功能,它允许我们基于响应式数据创建派生状态。计算属性具有缓存机制,只有当依赖的响应式数据变化时才会重新计算。本集我们将深入探讨计算属性的实现原理、使用方法和最佳实践。
一、计算属性的基本概念
1. 什么是计算属性?
计算属性是基于响应式依赖进行缓存的属性。当依赖的响应式数据发生变化时,计算属性会自动重新计算;如果依赖没有变化,计算属性会直接返回缓存的结果。
2. 计算属性的优势
- 缓存机制:避免重复计算,提高性能
- 声明式语法:更清晰地表达数据之间的依赖关系
- 响应式更新:自动追踪依赖,当依赖变化时更新
- 更好的可读性:将复杂的计算逻辑从模板中分离出来
二、计算属性的基本使用
1. 在组合式API中使用
import { ref, computed } from 'vue'
const count = ref(0)
// 只读计算属性
const doubleCount = computed(() => {
return count.value * 2
})
// 可写计算属性
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
const [first, last] = newValue.split(' ')
firstName.value = first
lastName.value = last
}
})2. 在模板中使用
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double Count: {{ doubleCount }}</p>
<p>Full Name: {{ fullName }}</p>
<input v-model="fullName" placeholder="Enter full name" />
</div>
</template>三、计算属性的实现原理
1. 缓存机制
计算属性的核心特性是缓存。Vue会跟踪计算属性依赖的响应式数据,当这些依赖变化时,才会重新计算。
// 计算属性只会在count变化时重新计算
doubleCount.value // 第一次计算,缓存结果
count.value++ // 依赖变化
count.value++ // 依赖变化
count.value++ // 依赖变化
doubleCount.value // 重新计算,缓存新结果
doubleCount.value // 直接返回缓存结果,不重新计算2. 计算属性的工作流程
- 初始化:创建计算属性时,Vue会记录其getter函数
- 首次访问:当首次访问计算属性时,执行getter函数并缓存结果
- 依赖收集:在执行getter函数过程中,Vue会自动收集所有访问的响应式数据作为依赖
- 依赖变化:当任何依赖发生变化时,计算属性会被标记为"脏"(需要重新计算)
- 再次访问:当再次访问计算属性时,如果它是"脏"的,就会重新执行getter函数并更新缓存,否则直接返回缓存结果
3. 简化的实现原理
function computed(getterOrOptions) {
let getter
let setter
// 处理传入的参数
if (typeof getterOrOptions === 'function') {
getter = getterOrOptions
setter = () => {
console.warn('Write operation failed: computed value is readonly')
}
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
let dirty = true // 标记是否需要重新计算
let value // 缓存的计算结果
let scheduler = null // 调度函数
// 创建副作用函数
const effect = () => {
value = getter()
dirty = false
}
// 依赖收集和触发更新的逻辑
const computedRef = {
get value() {
if (dirty) {
// 执行getter,收集依赖
effect()
}
// 依赖收集:将当前计算属性添加到依赖的响应式数据的依赖列表中
track(computedRef, 'value')
return value
},
set value(newValue) {
setter(newValue)
}
}
return computedRef
}四、计算属性与methods的区别
| 特性 | 计算属性 | Methods |
|---|---|---|
| 缓存机制 | 有,依赖不变时返回缓存结果 | 无,每次调用都重新执行 |
| 调用方式 | 在模板中直接使用,不需要括号 | 需要括号调用 |
| 响应式更新 | 自动追踪依赖,依赖变化时更新 | 每次渲染都重新执行 |
| 适用场景 | 基于现有数据的派生值 | 事件处理或复杂的命令式逻辑 |
1. 性能对比
// 计算属性 - 只计算一次,除非deps变化
const expensiveComputed = computed(() => {
console.log('Computing expensive value...')
// 模拟昂贵的计算
for (let i = 0; i < 1000000; i++) {}
return count.value * 2
})
// Methods - 每次调用都计算
const expensiveMethod = () => {
console.log('Computing expensive value...')
// 模拟昂贵的计算
for (let i = 0; i < 1000000; i++) {}
return count.value * 2
}2. 何时使用计算属性 vs Methods
- 计算属性:当你需要基于响应式数据派生一个值,并且这个值可能会被多次访问时
- Methods:当你需要执行命令式逻辑,或者每次调用都需要重新计算时
五、计算属性的高级用法
1. 可写计算属性
计算属性默认是只读的,但可以通过提供setter函数来创建可写的计算属性:
import { ref, computed } from 'vue'
const firstName = ref('Alice')
const lastName = ref('Smith')
const fullName = computed({
get() {
return `${firstName.value} ${lastName.value}`
},
set(newValue) {
const [first, last] = newValue.split(' ')
firstName.value = first
lastName.value = last
}
})
// 设置计算属性会触发setter
fullName.value = 'Bob Johnson' // 会将firstName设为'Bob',lastName设为'Johnson'2. 计算属性的依赖跟踪
计算属性会自动跟踪其getter函数中访问的所有响应式数据:
import { ref, computed } from 'vue'
const a = ref(1)
const b = ref(2)
const c = ref(3)
const sum = computed(() => {
console.log('Computing sum...')
return a.value + b.value + c.value
})
// 当a、b或c变化时,sum会重新计算3. 计算属性与watch的结合使用
在某些复杂场景下,可以结合使用计算属性和watch:
import { ref, computed, watch } from 'vue'
const radius = ref(5)
// 计算属性:计算圆的面积
const area = computed(() => {
return Math.PI * radius.value * radius.value
})
// watch:监听面积变化,执行副作用
watch(area, (newArea, oldArea) => {
console.log(`Area changed from ${oldArea} to ${newArea}`)
// 可以执行一些副作用,如发送请求、更新DOM等
})4. 计算属性的嵌套使用
计算属性可以依赖其他计算属性:
import { ref, computed } from 'vue'
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
const tripleCount = computed(() => doubleCount.value * 1.5) // 依赖doubleCount
const quadrupleCount = computed(() => tripleCount.value * (4/3)) // 依赖tripleCount5. 计算属性与toRefs结合
import { reactive, computed, toRefs } from 'vue'
const user = reactive({
firstName: 'Alice',
lastName: 'Smith',
age: 30
})
const fullName = computed(() => `${user.firstName} ${user.lastName}`)
const isAdult = computed(() => user.age >= 18)
// 结合toRefs返回
const userRefs = {
...toRefs(user),
fullName,
isAdult
}六、计算属性的最佳实践
1. 保持计算属性简洁
计算属性应该只做一件事,并且保持简洁。如果计算逻辑复杂,应该考虑将其拆分为多个计算属性,或者提取为单独的函数:
// 不推荐:复杂的计算逻辑
const complexResult = computed(() => {
let result = 0
for (let i = 0; i < items.value.length; i++) {
if (items.value[i].active) {
result += items.value[i].price * items.value[i].quantity
}
}
return result * (1 - discount.value / 100)
})
// 推荐:拆分为多个计算属性
const subtotal = computed(() => {
return items.value
.filter(item => item.active)
.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
const total = computed(() => {
return subtotal.value * (1 - discount.value / 100)
})2. 避免在计算属性中产生副作用
计算属性应该是纯函数,不应该产生副作用(如修改DOM、发送请求、修改其他响应式数据等):
// 不推荐:在计算属性中产生副作用
const fullName = computed(() => {
const name = `${firstName.value} ${lastName.value}`
// 副作用:修改其他响应式数据
document.title = name
// 副作用:发送请求
fetch(`/api/user/${name}`)
return name
})
// 推荐:使用watch处理副作用
const fullName = computed(() => `${firstName.value} ${lastName.value}`)
watch(fullName, (newName) => {
document.title = newName
fetch(`/api/user/${newName}`)
})3. 注意计算属性的依赖跟踪
计算属性只会跟踪其getter函数中显式访问的响应式数据:
import { ref, computed } from 'vue'
const items = ref([1, 2, 3])
const index = ref(0)
// 计算属性只会跟踪index的变化,不会跟踪items的变化
const currentItem = computed(() => {
return items.value[index.value]
})
// 修改index会触发currentItem重新计算
index.value++ // currentItem重新计算
// 修改items不会触发currentItem重新计算,除非index指向的元素变化
items.value.push(4) // currentItem不会重新计算
items.value[1] = 100 // currentItem不会重新计算,除非index是14. 合理使用可写计算属性
可写计算属性应该用于双向绑定的场景,如表单处理:
import { ref, computed } from 'vue'
const user = reactive({
firstName: 'Alice',
lastName: 'Smith'
})
// 可写计算属性,用于双向绑定
const fullName = computed({
get() {
return `${user.firstName} ${user.lastName}`
},
set(newValue) {
const [first, last] = newValue.split(' ')
user.firstName = first || ''
user.lastName = last || ''
}
})七、计算属性的性能优化
1. 避免不必要的计算
计算属性的缓存机制已经提供了很好的性能优化,但在某些情况下,我们还可以进一步优化:
- 避免在计算属性中执行昂贵的操作:如复杂的循环、DOM操作、网络请求等
- 拆分复杂的计算属性:将一个复杂的计算属性拆分为多个简单的计算属性
- 使用v-once或v-memo:在模板中,对于不经常变化的计算属性,可以使用v-once或v-memo进行缓存
2. 注意大型数组的计算
当计算属性依赖大型数组时,要注意性能问题:
// 不推荐:对大型数组进行频繁的计算
const filteredItems = computed(() => {
return largeArray.value.filter(item => {
// 复杂的过滤逻辑
return item.name.includes(searchQuery.value) &&
item.category === selectedCategory.value &&
item.price >= minPrice.value &&
item.price <= maxPrice.value
})
})
// 推荐:使用防抖或节流
import { ref, computed, watch } from 'vue'
const searchQuery = ref('')
const selectedCategory = ref('')
const minPrice = ref(0)
const maxPrice = ref(1000)
// 防抖后的搜索查询
const debouncedSearchQuery = ref('')
// 使用watch监听搜索条件变化,防抖更新
watch([searchQuery, selectedCategory, minPrice, maxPrice],
debounce(([newQuery, newCategory, newMin, newMax]) => {
debouncedSearchQuery.value = newQuery
// 更新其他过滤条件...
}, 300)
)
// 基于防抖后的搜索条件进行计算
const filteredItems = computed(() => {
return largeArray.value.filter(item => {
return item.name.includes(debouncedSearchQuery.value) &&
item.category === selectedCategory.value &&
item.price >= minPrice.value &&
item.price <= maxPrice.value
})
})八、计算属性的常见错误
1. 忘记使用.value
在组合式API中,计算属性返回的是一个ref对象,需要使用.value访问:
// 错误示例
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount) // 输出:ComputedRefImpl { ... }
// 正确示例
const doubleCount = computed(() => count.value * 2)
console.log(doubleCount.value) // 输出:02. 在计算属性中修改依赖
在计算属性的getter函数中修改依赖会导致无限循环:
// 错误示例:无限循环
const count = ref(0)
const incrementedCount = computed(() => {
count.value++ // 在getter中修改依赖,会导致无限循环
return count.value
})
// 正确示例:使用watch或methods
const count = ref(0)
const incrementedCount = computed(() => count.value)
watch(count, (newCount) => {
// 在watch中处理副作用
console.log(`Count changed to ${newCount}`)
})3. 过度使用计算属性
计算属性适用于基于响应式数据的派生值,但不适用于所有场景:
// 不推荐:简单的计算,直接在模板中使用表达式
const doubleCount = computed(() => count.value * 2)
// 推荐:对于简单的计算,可以直接在模板中使用
// {{ count * 2 }}4. 混淆计算属性和watch
计算属性用于派生值,watch用于处理副作用:
// 不推荐:使用watch派生值
const doubleCount = ref(0)
watch(count, (newCount) => {
doubleCount.value = newCount * 2
})
// 推荐:使用计算属性派生值
const doubleCount = computed(() => count.value * 2)九、计算属性与相关API的比较
1. 计算属性 vs watch
| 特性 | 计算属性 | Watch |
|---|---|---|
| 用途 | 派生值 | 处理副作用 |
| 缓存 | 有 | 无 |
| 语法 | 声明式 | 命令式 |
| 自动执行 | 首次访问时执行 | 依赖变化时执行 |
| 适用场景 | 基于现有数据计算新值 | 监听数据变化执行副作用 |
2. 计算属性 vs methods
| 特性 | 计算属性 | Methods |
|---|---|---|
| 缓存 | 有 | 无 |
| 调用方式 | 直接访问 | 函数调用 |
| 响应式 | 自动追踪依赖 | 每次调用重新执行 |
| 适用场景 | 派生值 | 命令式逻辑 |
3. 计算属性 vs ref
| 特性 | 计算属性 | Ref |
|---|---|---|
| 初始化 | 基于getter函数 | 基于初始值 |
| 缓存 | 有 | 无 |
| 更新机制 | 依赖变化时自动更新 | 手动更新.value |
| 适用场景 | 派生值 | 响应式基本类型或对象 |
十、计算属性的高级特性
1. 计算属性的惰性计算
计算属性是惰性的,只有在被访问时才会首次计算:
import { ref, computed } from 'vue'
const count = ref(0)
const expensiveComputation = computed(() => {
console.log('Performing expensive computation...')
// 模拟昂贵的计算
for (let i = 0; i < 1000000; i++) {}
return count.value * 2
})
// 此时expensiveComputation还没有执行
console.log('Before accessing expensiveComputation')
// 首次访问,执行计算
console.log(expensiveComputation.value)
// 再次访问,直接返回缓存结果
console.log(expensiveComputation.value)2. 计算属性的调试
可以使用debugger或console.log在计算属性的getter函数中进行调试:
const fullName = computed(() => {
debugger // 断点调试
console.log(`Computing fullName from ${firstName.value} and ${lastName.value}`)
return `${firstName.value} ${lastName.value}`
})3. 计算属性的类型推断
在TypeScript中,计算属性会自动推断返回类型:
import { ref, computed } from 'vue'
const count = ref(0)
// doubleCount的类型会被推断为number
const doubleCount = computed(() => count.value * 2)
// 也可以显式指定类型
const tripleCount = computed<number>(() => count.value * 3)十一、总结
1. 核心概念
- 计算属性是基于响应式依赖进行缓存的属性
- 计算属性具有缓存机制,只有当依赖变化时才会重新计算
- 计算属性可以是只读的,也可以是可写的
- 计算属性应该是纯函数,避免产生副作用
2. 使用原则
- 当需要基于响应式数据派生新值时,使用计算属性
- 当计算逻辑复杂或需要多次使用结果时,使用计算属性
- 当需要执行副作用时,使用watch或methods
- 保持计算属性简洁,避免复杂逻辑
3. 最佳实践
- 保持计算属性简洁,只做一件事
- 避免在计算属性中产生副作用
- 合理使用缓存机制,避免重复计算
- 结合使用计算属性和watch处理复杂场景
- 注意计算属性的依赖跟踪,确保依赖变化时能正确更新
4. 性能优化
- 避免在计算属性中执行昂贵的操作
- 对于大型数组的计算,使用防抖或节流
- 合理拆分复杂的计算属性
- 避免过度使用计算属性,简单计算可以直接在模板中使用
十二、练习题
基础练习:
- 创建一个计数器,使用计算属性计算其平方、立方和平方根
- 在模板中展示这些计算结果
进阶练习:
- 实现一个购物车功能,使用计算属性计算商品总价、折扣和最终价格
- 使用可写计算属性实现商品数量的双向绑定
- 结合watch实现购物车总价变化时的提示
性能优化练习:
- 实现一个搜索功能,使用计算属性过滤大型数组
- 添加防抖机制,优化搜索性能
- 对比使用计算属性和直接在模板中过滤的性能差异
综合练习:
- 实现一个用户管理系统,使用计算属性处理用户数据
- 实现用户筛选、排序和分页功能
- 使用计算属性统计用户数量、平均年龄等信息
十三、扩展阅读
在下一集中,我们将学习"侦听器watch与watchEffect",深入探讨Vue 3中的数据监听机制。