Vue computed 和 watch
Vue 提供了两个重要的 API 来处理响应式数据的变化:computed 用于创建计算属性,watch 用于监听数据变化并执行相应的操作。
computed()
computed() 用于创建计算属性,它基于响应式数据进行计算,并缓存结果。
基本用法
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 创建计算属性
const fullName = computed(() => {
return firstName.value + lastName.value
})
console.log(fullName.value) // '张三'
// 修改依赖的数据
firstName.value = '李'
console.log(fullName.value) // '李三'
计算属性 vs 方法
计算属性和方法的主要区别在于缓存:
import { ref, computed, Date } from 'vue'
const items = ref([1, 2, 3, 4, 5])
// 计算属性 - 有缓存,只有依赖变化时才重新计算
const sum = computed(() => {
console.log('计算中...') // 只执行一次
return items.value.reduce((a, b) => a + b, 0)
})
// 方法 - 每次调用都会执行
function getSum() {
console.log('计算中...') // 每次调用都执行
return items.value.reduce((a, b) => a + b, 0)
}
<template>
<p>计算属性: {{ sum }}</p> <!-- 只计算一次 -->
<p>计算属性: {{ sum }}</p> <!-- 使用缓存 -->
<p>方法: {{ getSum() }}</p> <!-- 每次都重新计算 -->
<p>方法: {{ getSum() }}</p> <!-- 每次都重新计算 -->
</template>
可写的计算属性
计算属性默认是只读的,但可以通过 get 和 set 创建可写的计算属性:
import { ref, computed } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
const fullName = computed({
get() {
return firstName.value + lastName.value
},
set(value) {
// 当设置 fullName 时,自动拆分到 firstName 和 lastName
firstName.value = value.slice(0, 1)
lastName.value = value.slice(1)
}
})
console.log(fullName.value) // '张三'
fullName.value = '李四'
console.log(firstName.value) // '李'
console.log(lastName.value) // '四'
实际示例:购物车
import { ref, computed } from 'vue'
const cart = ref([
{ name: '苹果', price: 5, quantity: 2 },
{ name: '香蕉', price: 3, quantity: 1 },
{ name: '橙子', price: 4, quantity: 3 }
])
// 计算总数量
const totalQuantity = computed(() => {
return cart.value.reduce((sum, item) => sum + item.quantity, 0)
})
// 计算总价
const totalPrice = computed(() => {
return cart.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
})
// 计算折扣后价格
const discountedPrice = computed({
get() {
return totalPrice.value * 0.9 // 9折
},
set(value) {
// 根据折扣后价格反推原价
const originalPrice = value / 0.9
console.log('原价:', originalPrice.toFixed(2))
}
})
watch()
watch() 用于监听数据变化并执行相应的操作。
基本用法
import { ref, watch } from 'vue'
const count = ref(0)
// 监听 count 的变化
watch(count, (newValue, oldValue) => {
console.log(`count 变化: ${oldValue} -> ${newValue}`)
})
count.value++ // 输出: count 变化: 0 -> 1
监听多个数据源
import { ref, watch } from 'vue'
const firstName = ref('张')
const lastName = ref('三')
// 监听多个数据源
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`${oldFirst} -> ${newFirst}, ${oldLast} -> ${newLast}`)
})
firstName.value = '李' // 输出: 张 -> 李, 三 -> 三
深度监听
import { ref, reactive, watch } from 'vue'
const state = reactive({
user: {
name: '张三',
age: 25
}
})
// 深度监听
watch(state, (newValue, oldValue) => {
console.log('state 变化:', newValue)
}, { deep: true })
state.user.age = 30 // 触发监听
立即执行
import { ref, watch } from 'vue'
const message = ref('')
// 添加 immediate 选项,立即执行一次
watch(message, (newValue) => {
console.log('消息:', newValue)
}, { immediate: true })
// 输出: 消息: (初始值)
获取新值和旧值
import { reactive, watch } from 'vue'
const state = reactive({ count: 0 })
watch(state, (newValue, oldValue) => {
console.log('新值:', newValue)
console.log('旧值:', oldValue)
// 注意:对于 reactive 对象,新值和旧值是同一个对象
})
state.count = 1
// 新值: { count: 1 }
// 旧值: { count: 1 } // 相同引用!
监听特定属性
import { reactive, watch } from 'vue'
const state = reactive({
user: {
name: '张三',
age: 25
}
})
// 只监听 user.name 的变化
watch(() => state.user.name, (newValue, oldValue) => {
console.log(`name: ${oldValue} -> ${newValue}`)
})
state.user.name = '李四' // 触发
state.user.age = 30 // 不触发
watchEffect()
watchEffect() 会立即执行传入的回调函数,并自动追踪回调中使用的所有响应式数据。
基本用法
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('张三')
watchEffect(() => {
console.log(`${name.value}: ${count.value}`)
})
count.value++ // 输出: 张三: 1
name.value = '李四' // 输出: 李四: 1
watch vs watchEffect
| 特性 | watch | watchEffect |
|---|---|---|
| 惰性执行 | 否(默认惰性,可配置 immediate) | 是(立即执行) |
| 获取新值/旧值 | 支持 | 不支持 |
| 明确指定依赖 | 需要(第一个参数) | 自动追踪 |
| 适用场景 | 需要获取变化前后的值 | 只需要响应式依赖变化 |
选择建议
// 使用 watchEffect
// - 不关心旧值
// - 想在初始化时执行
// - 依赖明确,不需要显式指定
watchEffect(() => {
console.log(searchQuery.value)
fetchResults(searchQuery.value)
})
// 使用 watch
// - 需要访问旧值
// - 只在特定条件下监听
// - 明确的依赖关系
watch(searchQuery, (newValue, oldValue) => {
console.log(`${oldValue} -> ${newValue}`)
}, { immediate: true })
停止监听
自动停止
在 setup() 或 <script setup> 中使用 watch,组件卸载时会自动停止:
<script setup>
import { watch } from 'vue'
const count = ref(0)
watch(count, () => {
console.log(count.value)
})
// 组件卸载时自动停止
</script>
手动停止
使用 watch 返回的停止函数:
import { watch } from 'vue'
const count = ref(0)
const stop = watch(count, () => {
console.log(count.value)
})
// 停止监听
stop()
count.value++ // 不再触发
实际示例:搜索功能
<script setup>
import { ref, watch, watchEffect } from 'vue'
const searchQuery = ref('')
const results = ref([])
const isLoading = ref(false)
// 使用 watchEffect 自动搜索
watchEffect(async () => {
if (!searchQuery.value) {
results.value = []
return
}
isLoading.value = true
try {
const response = await fetch(`/api/search?q=${searchQuery.value}`)
results.value = await response.json()
} finally {
isLoading.value = false
}
})
// 使用 watch 监听特定变化
watch(results, (newResults) => {
console.log('搜索结果更新:', newResults.length)
}, { deep: true })
</script>
<template>
<div>
<input v-model="searchQuery" placeholder="搜索..." />
<p v-if="isLoading">加载中...</p>
<ul>
<li v-for="result in results" :key="result.id">
{{ result.name }}
</li>
</ul>
</div>
</template>
示例:表单验证
<script setup>
import { ref, computed, watch } from 'vue'
const email = ref('')
const errors = ref({
email: ''
})
const isValidEmail = computed(() => {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
return emailRegex.test(email.value)
})
watch(email, (newValue) => {
if (!newValue) {
errors.value.email = '邮箱不能为空'
} else if (!isValidEmail.value) {
errors.value.email = '邮箱格式不正确'
} else {
errors.value.email = ''
}
})
const isFormValid = computed(() => {
return !errors.value.email
})
</script>
<template>
<form>
<div>
<input v-model="email" type="email" placeholder="邮箱" />
<span v-if="errors.email" class="error">{{ errors.email }}</span>
</div>
<button :disabled="!isFormValid">提交</button>
</div>
</template>
小结
本章我们详细学习了 computed 和 watch 的完整内容:
- computed():创建计算属性,自动缓存
- 可写计算属性:使用 get 和 set
- watch():监听数据变化,获取新旧值
- watchEffect():自动追踪依赖的响应式数据
- 深度监听:监听对象深层变化
- 停止监听:自动和手动停止
练习
- 创建一个实时搜索的组件
- 实现一个表单验证系统
- 创建一个计算器组件
准备好进入下一章,学习组合式 API 的高级用法的内容了吗?