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() 支持调试选项,可以在开发模式下追踪依赖关系:
onTrack 和 onTrigger
import { ref, computed } from 'vue'
const count = ref(0)
const plusOne = computed(() => count.value + 1, {
// 当依赖被追踪时调用
onTrack(e) {
console.log('追踪依赖:', e)
// e = { effect, target, type, key }
debugger // 可以设置断点调试
},
// 当依赖变化触发重新计算时调用
onTrigger(e) {
console.log('触发重新计算:', e)
// e = { effect, target, type, key, newValue, oldValue }
debugger
}
})
// 访问计算属性,会触发 onTrack
console.log(plusOne.value) // onTrack 被调用
// 修改依赖,会触发 onTrigger
count.value = 1 // onTrigger 被调用
调试事件对象:
type DebuggerEvent = {
effect: ReactiveEffect // 当前的副作用函数
target: object // 响应式对象
type: TrackOpTypes // 操作类型: 'get' | 'has' | 'iterate'
key: any // 属性键
newValue?: any // 新值(仅 onTrigger)
oldValue?: any // 旧值(仅 onTrigger)
}
watch 高级选项
flush 选项
控制回调的触发时机:
import { watch, watchEffect } from 'vue'
const count = ref(0)
// 'pre'(默认):组件更新前执行
watch(count, () => {
// 在组件更新前执行
// DOM 还是旧的状态
}, { flush: 'pre' })
// 'post':组件更新后执行
watch(count, () => {
// 在组件更新后执行
// DOM 已更新,可以安全访问
}, { flush: 'post' })
// 'sync':同步执行
watch(count, () => {
// 响应式数据变化后立即执行
// 注意:可能导致性能问题和数据一致性问题
}, { flush: 'sync' })
watchPostEffect 和 watchSyncEffect
Vue 提供了便捷的别名:
import { watchPostEffect, watchSyncEffect } from 'vue'
// 等价于 watchEffect(() => {}, { flush: 'post' })
watchPostEffect(() => {
// DOM 更新后执行
console.log(document.getElementById('count')?.textContent)
})
// 等价于 watchEffect(() => {}, { flush: 'sync' })
watchSyncEffect(() => {
// 同步执行,谨慎使用!
console.log('同步执行')
})
once 选项(Vue 3.4+)
回调只触发一次:
import { watch } from 'vue'
const count = ref(0)
watch(count, (newValue, oldValue) => {
console.log(`只执行一次: ${oldValue} -> ${newValue}`)
}, { once: true })
count.value = 1 // 输出: 只执行一次: 0 -> 1
count.value = 2 // 不再触发
deep 选项详解
深度监听对象的所有嵌套属性变化:
import { reactive, watch } from 'vue'
const state = reactive({
user: {
name: '张三',
address: {
city: '北京'
}
}
})
// 监听整个对象
watch(
() => state,
(newValue, oldValue) => {
// 注意:深度监听时,newValue 和 oldValue 是同一个对象
console.log('状态变化:', newValue)
console.log(newValue === oldValue) // true
},
{ deep: true }
)
// 也可以指定深度层级(Vue 3.5+)
watch(
() => state,
() => {
console.log('状态变化')
},
{ deep: 2 } // 只监听 2 层深度
)
state.user.name = '李四' // 触发
state.user.address.city = '上海' // 触发
暂停和恢复监听(Vue 3.5+)
可以临时暂停和恢复监听器:
import { watch, ref } from 'vue'
const count = ref(0)
const { stop, pause, resume } = watch(count, (newValue) => {
console.log('count:', newValue)
})
count.value = 1 // 输出: count: 1
// 暂停监听
pause()
count.value = 2 // 不触发
// 恢复监听
resume()
count.value = 3 // 输出: count: 3
// 停止监听(永久)
stop()
count.value = 4 // 不触发
副作用清理
onCleanup 回调
当监听的值变化时,可能需要清理之前的副作用:
import { ref, watch } from 'vue'
const id = ref(1)
watch(id, async (newId, oldId, onCleanup) => {
// 创建一个可取消的请求
const controller = new AbortController()
// 注册清理函数
// 当 id 再次变化或监听器停止时,清理函数会被调用
onCleanup(() => {
controller.abort() // 取消正在进行的请求
console.log('清理之前的请求')
})
try {
const response = await fetch(`/api/user/${newId}`, {
signal: controller.signal
})
const data = await response.json()
console.log('获取数据:', data)
} catch (e) {
if (e.name === 'AbortError') {
console.log('请求被取消')
}
}
})
watchEffect 中的清理
import { ref, watchEffect } from 'vue'
const id = ref(1)
watchEffect(async (onCleanup) => {
const controller = new AbortController()
onCleanup(() => {
controller.abort()
})
const response = await fetch(`/api/data/${id.value}`, {
signal: controller.signal
})
const data = await response.json()
// 处理数据
})
// 或者使用 Vue 3.5+ 的 onWatcherCleanup
import { onWatcherCleanup } from 'vue'
watchEffect(async () => {
const controller = new AbortController()
onWatcherCleanup(() => {
controller.abort()
})
const response = await fetch(`/api/data/${id.value}`, {
signal: controller.signal
})
})
实际应用:防抖搜索
import { ref, watch } from 'vue'
const searchQuery = ref('')
const results = ref([])
watch(searchQuery, (newQuery, oldQuery, onCleanup) => {
let cancelled = false
// 设置超时防抖
const timeoutId = setTimeout(async () => {
if (cancelled) return
const response = await fetch(`/api/search?q=${newQuery}`)
if (!cancelled) {
results.value = await response.json()
}
}, 300)
// 清理函数:取消之前的搜索
onCleanup(() => {
cancelled = true
clearTimeout(timeoutId)
})
})
实际应用示例
实时数据同步
<script setup>
import { ref, watch, onCleanup } from 'vue'
const userId = ref(1)
const userData = ref(null)
const lastSyncTime = ref(null)
watch(userId, async (newId, oldId, onCleanup) => {
let cancelled = false
const controller = new AbortController()
onCleanup(() => {
cancelled = true
controller.abort()
})
try {
const response = await fetch(`/api/user/${newId}`, {
signal: controller.signal
})
if (!cancelled) {
userData.value = await response.json()
lastSyncTime.value = new Date().toLocaleTimeString()
}
} catch (e) {
if (e.name !== 'AbortError') {
console.error('获取用户数据失败:', e)
}
}
}, { immediate: true })
</script>
复杂表单验证
<script setup>
import { ref, computed, watch } from 'vue'
const form = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const errors = reactive({
username: '',
email: '',
password: '',
confirmPassword: ''
})
const touched = reactive({
username: false,
email: false,
password: false,
confirmPassword: false
})
// 用户名验证
watch(
() => form.username,
(value) => {
if (touched.username) {
if (!value) {
errors.username = '用户名不能为空'
} else if (value.length < 3) {
errors.username = '用户名至少3个字符'
} else {
errors.username = ''
}
}
}
)
// 邮箱验证
watch(
() => form.email,
(value) => {
if (touched.email) {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
if (!value) {
errors.email = '邮箱不能为空'
} else if (!emailRegex.test(value)) {
errors.email = '邮箱格式不正确'
} else {
errors.email = ''
}
}
}
)
// 密码验证
watch(
[() => form.password, () => form.confirmPassword],
([password, confirmPassword]) => {
if (touched.password) {
if (!password) {
errors.password = '密码不能为空'
} else if (password.length < 6) {
errors.password = '密码至少6个字符'
} else {
errors.password = ''
}
}
if (touched.confirmPassword) {
if (password !== confirmPassword) {
errors.confirmPassword = '两次密码不一致'
} else {
errors.confirmPassword = ''
}
}
}
)
const isFormValid = computed(() => {
return !Object.values(errors).some(e => e)
})
function handleBlur(field) {
touched[field] = true
}
function submit() {
if (isFormValid.value) {
console.log('提交表单:', form)
}
}
</script>
小结
本章我们详细学习了 computed 和 watch 的完整内容:
- computed():创建计算属性,自动缓存
- 可写计算属性:使用 get 和 set
- computed 调试:onTrack 和 onTrigger 调试选项
- watch():监听数据变化,获取新旧值
- watchEffect():自动追踪依赖的响应式数据
- watch 高级选项:flush、deep、immediate、once
- watchPostEffect/watchSyncEffect:不同执行时机的便捷别名
- 暂停和恢复:pause/resume 功能
- 副作用清理:onCleanup 处理异步操作取消
- 深度监听:监听对象深层变化
练习
- 创建一个实时搜索的组件,使用副作用清理处理请求取消
- 实现一个复杂的表单验证系统,支持实时验证和提交验证
- 创建一个数据同步组件,支持暂停和恢复同步
- 实现一个带防抖的自动保存功能
准备好进入下一章,学习组合式 API 的高级用法的内容了吗?