1. 概述
在Vue.js中,computed
和watch
都是响应式数据变化的重要机制,但它们的设计目的和使用场景有所不同。理解两者的区别对于编写高效、可维护的Vue应用至关重要。
2. 计算属性(computed)
2.1 定义
计算属性是基于其响应式依赖项的值计算得出的属性,具有缓存特性。
2.2 基本语法
// Vue 2
computed: {
fullName() {
return this.firstName + ' ' + this.lastName
}
}
// Vue 3 Composition API
import { computed, ref } from 'vue'
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => firstName.value + ' ' + lastName.value)
2.3 特点
- 缓存性:只有当依赖项发生变化时才会重新计算
- 声明式:描述的是数据之间的关系
- 同步执行:计算过程是同步的
- 返回值:必须返回一个值
- 只读性:默认情况下是只读的(可以通过getter/setter改变)
2.4 详细示例
// Vue 3 组合式API示例
<template>
<div>
<p>名字:{{ firstName }}</p>
<p>姓氏:{{ lastName }}</p>
<p>全名:{{ fullName }}</p>
<p>全名长度:{{ fullNameLength }}</p>
<p>价格总计:{{ totalPrice }}</p>
</div>
</template>
<script setup>
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const products = ref([
{ name: '商品1', price: 100, quantity: 2 },
{ name: '商品2', price: 200, quantity: 1 }
])
// 基础计算属性
const fullName = computed(() => {
console.log('计算fullName') // 只有依赖变化时才会执行
return firstName.value + lastName.value
})
// 依赖多个数据的计算属性
const fullNameLength = computed(() => {
return fullName.value.length
})
// 复杂计算
const totalPrice = computed(() => {
return products.value.reduce((total, product) => {
return total + product.price * product.quantity
}, 0)
})
// 可写计算属性
const fullNameWritable = computed({
get() {
return firstName.value + ' ' + lastName.value
},
set(value) {
const names = value.split(' ')
firstName.value = names[0]
lastName.value = names[1]
}
})
</script>
2. 侦听器(watch)
2.1 定义
侦听器用于观察和响应Vue实例上的数据变动,当需要在数据变化时执行异步或开销较大的操作时使用。
2.2 基本语法
// Vue 2
watch: {
question(newVal, oldVal) {
this.getAnswer()
}
}
// Vue 3 Composition API
import { watch, ref } from 'vue'
const question = ref('')
watch(question, (newVal, oldVal) => {
// 处理变化
})
2.3 特点
- 命令式:描述的是当数据变化时要做什么
- 无缓存:每次依赖变化都会执行
- 异步支持:可以执行异步操作
- 无返回值:不需要返回值
- 副作用:通常用于执行副作用操作
2.4 详细示例
// Vue 3 组合式API示例
<template>
<div>
<input v-model="question" placeholder="请输入问题">
<p>{{ answer }}</p>
<p>搜索历史:{{ searchHistory }}</p>
</div>
</template>
<script setup>
import { ref, watch, watchEffect } from 'vue'
const question = ref('')
const answer = ref('')
const searchHistory = ref([])
// 基础侦听器
watch(question, (newQuestion, oldQuestion) => {
console.log(`问题从 "${oldQuestion}" 变为 "${newQuestion}"`)
if (newQuestion) {
getAnswer(newQuestion)
}
})
// 侦听多个数据源
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('姓名发生变化')
})
// 深度侦听
const user = ref({
name: '张三',
age: 25,
address: {
city: '北京',
street: '长安街'
}
})
watch(user, (newUser, oldUser) => {
console.log('用户信息变化', newUser)
}, { deep: true })
// 立即执行侦听器
watch(question, (newVal) => {
// 处理逻辑
}, { immediate: true })
// watchEffect - 自动追踪依赖
watchEffect(() => {
if (question.value) {
searchHistory.value.push(question.value)
}
})
// 异步操作示例
async function getAnswer(question) {
answer.value = '搜索中...'
try {
// 模拟API调用
await new Promise(resolve => setTimeout(resolve, 1000))
answer.value = `对于问题"${question}"的答案是...`
} catch (error) {
answer.value = '搜索失败'
}
}
</script>
3. 核心区别
3.1 设计目的
特性 | Computed | Watch |
---|---|---|
目的 | 声明式的数据转换 | 命令式的副作用处理 |
关注点 | 数据的计算和派生 | 数据变化的响应 |
3.2 执行时机
// computed - 惰性计算
const expensiveValue = computed(() => {
console.log('计算执行') // 只有在被访问且依赖变化时才执行
return heavyComputation()
})
// watch - 即时执行
watch(someData, () => {
console.log('侦听器执行') // 每次依赖变化都会执行
})
3.3 缓存机制
// computed 有缓存
const computedValue = computed(() => {
console.log('计算执行')
return Math.random()
})
// 多次访问会使用缓存
console.log(computedValue.value) // 计算执行,返回值如: 0.123
console.log(computedValue.value) // 使用缓存,返回相同值: 0.123
// watch 无缓存
watch(someRef, () => {
console.log('每次变化都会执行')
})
3.4 返回值
// computed 必须返回值
const doubleCount = computed(() => {
return count.value * 2 // 必须返回
})
// watch 不需要返回值
watch(count, (newVal) => {
console.log('新值:', newVal)
// 不需要返回值
})
3.5 异步支持
// computed 不支持异步
const asyncComputed = computed(async () => {
// ❌ 错误:computed不支持异步
return await fetchData()
})
// watch 支持异步
watch(searchTerm, async (newTerm) => {
// ✅ 正确:watch支持异步
const results = await searchAPI(newTerm)
searchResults.value = results
})
4.使用场景
4.1 计算属性适用场景
- 数据格式化
const formattedDate = computed(() => {
return new Date(timestamp.value).toLocaleDateString()
})
- 数据过滤和排序
const filteredList = computed(() => {
return list.value.filter(item => item.active)
})
- 复杂计算
const totalAmount = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
4.2 侦听器适用场景
- 异步操作
watch(searchQuery, async (newQuery) => {
if (newQuery) {
loading.value = true
try {
const results = await api.search(newQuery)
searchResults.value = results
} finally {
loading.value = false
}
}
})
- 副作用处理
watch(user, (newUser) => {
// 保存到localStorage
localStorage.setItem('user', JSON.stringify(newUser))
}, { deep: true })
- 数据验证
watch(formData, (newData) => {
validateForm(newData)
}, { deep: true })
5. 最佳实践
5.1 选择原则
- 用computed:当你需要基于现有数据计算新数据时
- 用watch:当你需要在数据变化时执行副作用操作时
5.2 避免常见错误
// ❌ 错误:在computed中修改数据
const badComputed = computed(() => {
if (condition.value) {
otherData.value = 'changed' // 错误:产生副作用
}
return someValue.value
})
// ✅ 正确:在watch中处理副作用
watch(condition, (newVal) => {
if (newVal) {
otherData.value = 'changed'
}
})
5.3 组合使用
// 计算属性 + 侦听器的组合使用
const filteredData = computed(() => {
return data.value.filter(item =>
item.name.includes(searchTerm.value)
)
})
// 监听过滤结果变化
watch(filteredData, (newData) => {
// 当过滤结果变化时,滚动到顶部
scrollToTop()
// 记录统计信息
analytics.track('search_results_updated', {
count: newData.length
})
})
5.4 清理资源
// 在组件卸载时清理侦听器
import { onUnmounted } from 'vue'
const stopWatcher = watch(someData, callback)
onUnmounted(() => {
stopWatcher() // 清理侦听器
})
6. 总结
- 计算属性:用于声明式的数据转换,有缓存机制,适合数据计算和格式化
- 侦听器:用于命令式的副作用处理,无缓存机制,适合异步操作和副作用处理