在 Vue 3 中,watch
和 watchEffect
都是用于响应式地执行副作用操作的 API,但它们在用法和功能上有重要区别。下面通过具体示例进行详细解释:
🕵️♂️ 1. watchEffect
- 特点:立即执行传入的函数,并自动追踪函数内使用的响应式依赖
- 使用场景:不需要知道具体哪个值变化,只要依赖变化就执行操作
- 无旧值访问:无法获取变化前的值
javascript
import { ref, watchEffect } from 'vue'
const count = ref(0)
const name = ref('Alice')
// 自动追踪函数内使用的响应式依赖
watchEffect(() => {
console.log(`watchEffect: count=${count.value}, name=${name.value}`)
// 每当 count 或 name 变化时都会执行
})
// 触发执行:
count.value++ // 输出: "watchEffect: count=1, name=Alice"
name.value = 'Bob' // 输出: "watchEffect: count=1, name=Bob"
特性说明:
-
立即执行:初始化时就会执行一次
-
自动依赖收集:函数内用到的响应式变量都会被自动追踪
-
清理副作用 :
javascriptwatchEffect((onCleanup) => { const timer = setTimeout(() => { console.log('Delayed log') }, 1000) onCleanup(() => clearTimeout(timer)) // 清理上一次的副作用 })
🔍 2. watch
- 特点:需要显式指定监听的数据源,可以访问变化前后的值
- 使用场景:需要知道具体哪个值变化,需要访问旧值,需要惰性执行
- 更精细控制:支持 deep、immediate 等选项
基本用法:
javascript
import { ref, watch } from 'vue'
const count = ref(0)
// 监听单个 ref
watch(count, (newVal, oldVal) => {
console.log(`watch: count从 ${oldVal} → ${newVal}`)
})
count.value++ // 输出: "watch: count从 0 → 1"
监听多个源:
javascript
const count = ref(0)
const name = ref('Alice')
// 监听多个 ref
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`变化: count=${oldCount}→${newCount}, name=${oldName}→${newName}`)
})
count.value++ // 输出: "变化: count=0→1, name=Alice→Alice"
name.value = 'Bob' // 输出: "变化: count=1→1, name=Alice→Bob"
监听响应式对象:
javascript
const user = reactive({
name: 'Alice',
age: 25,
address: {
city: 'Beijing'
}
})
// 监听整个对象(需要 deep 选项)
watch(
() => ({ ...user }), // 或使用 toRefs
(newUser, oldUser) => {
console.log('用户信息变化', newUser)
},
{ deep: true }
)
// 监听特定属性
watch(
() => user.age,
(newAge, oldAge) => {
console.log(`年龄变化: ${oldAge} → ${newAge}`)
}
)
// 触发:
user.age = 26 // 输出: "年龄变化: 25 → 26"
user.address.city = 'Shanghai' // 会触发 deep watch
高级选项:
javascript
watch(
count,
(newVal) => {
console.log('带选项的watch:', newVal)
},
{
immediate: true, // 立即执行一次
flush: 'post', // 在DOM更新后执行
onTrack(e) { // 调试钩子
console.debug('依赖被追踪', e)
},
onTrigger(e) { // 调试钩子
console.debug('依赖触发变化', e)
}
}
)
⚖️ 3. 核心区别对比
特性 | watchEffect |
watch |
---|---|---|
初始执行 | 立即执行 | 默认不执行(可通过 immediate: true 启用) |
依赖收集 | 自动收集函数内所有响应式依赖 | 需要显式指定监听源 |
旧值访问 | ❌ 无法获取旧值 | ✅ 可获取旧值 |
监听多个源 | 自动处理多个依赖 | 需用数组语法 watch([src1, src2], ...) |
深度监听 | 总是深度监听(对象内部变化也会触发) | 需显式设置 { deep: true } |
调试工具 | 不支持 onTrack/onTrigger |
支持调试钩子 |
性能优化 | 适合简单逻辑 | 适合复杂监听场景 |
典型使用场景 | 日志、发送分析事件、DOM操作 | 表单验证、数据同步、复杂状态逻辑 |
🧪 4. 真实场景示例
场景1:自动保存表单(使用 watchEffect)
javascript
const formData = reactive({
title: '',
content: '',
lastSaved: null
})
// 当标题或内容变化时自动保存
watchEffect(async (onCleanup) => {
if (!formData.title && !formData.content) return
// 防抖处理
const timer = setTimeout(async () => {
const response = await saveToAPI(formData)
formData.lastSaved = new Date()
}, 1000)
onCleanup(() => clearTimeout(timer))
})
场景2:路由参数变化加载数据(使用 watch)
javascript
import { watch } from 'vue'
import { useRoute } from 'vue-router'
const route = useRoute()
const post = ref(null)
// 监听路由参数变化
watch(
() => route.params.id,
async (newId) => {
if (!newId) return
post.value = await fetchPost(newId)
},
{ immediate: true } // 初始加载
)
场景3:组合使用
javascript
const user = reactive({ name: 'Alice', age: 30 })
// 当用户年龄变化时执行特定逻辑
watch(
() => user.age,
(newAge) => {
if (newAge >= 18) {
console.log('用户已成年')
}
}
)
// 自动记录所有用户属性变化
watchEffect(() => {
console.log('用户状态:', JSON.stringify(user))
})
⚠️ 5. 重要注意事项
-
异步操作:
javascript// ✅ 正确:在 watch 中使用异步 watch(data, async (newVal) => { const result = await fetchData(newVal) }) // ❌ 错误:不能直接在 async 函数中使用 watchEffect 的清理 watchEffect(async (onCleanup) => { ... })
-
停止侦听:
javascriptconst stop = watchEffect(() => { ... }) const stopWatch = watch(source, callback) // 在需要时停止 stop() stopWatch()
-
Ref 解包:
javascriptconst obj = reactive({ count: ref(0) }) // ✅ 自动解包 watchEffect(() => console.log(obj.count)) // 直接访问值 // ❌ 错误:监听的是 ref 对象而非值 watch(obj.count, (val) => ...) // ✅ 正确:使用 getter 函数 watch(() => obj.count, (val) => ...)
-
DOM 更新时机:
javascriptwatchEffect(() => { console.log('DOM 状态:', document.getElementById('elem')) }, { flush: 'post' }) // 在 DOM 更新后执行
💎 总结选择建议
使用 watchEffect 当 |
使用 watch 当 |
---|---|
需要立即执行副作用 | 需要惰性执行(首次不执行) |
依赖多个值且不关心具体哪个变化 | 需要知道具体哪个值变化 |
不需要旧值 | 需要访问旧值进行比较 |
简单操作(如日志、DOM操作) | 复杂操作(如API请求、状态验证) |
对象深度变化监听(无需额外配置) | 需要控制监听深度(避免性能问题) |
经验法则:
- 优先使用
watchEffect
处理简单副作用 - 当需要精确控制或访问旧值时使用
watch
- 对于对象内部变化,
watchEffect
更简单,但watch
更可控