Vue 3 Watch 监视完全指南
一、watch 基础概念
watch 用于侦听响应式数据源的变化 ,并在变化时执行副作用函数。它默认是惰性 的(不会立即执行),且能获取新值和旧值。
基本语法
js
watch(source, callback, options)
- source:侦听源(ref、reactive、getter 函数、数组)
- callback :
(newVal, oldVal, onCleanup) => {} - options:配置项(immediate、deep、flush、once 等)
二、侦听不同数据源
1. 侦听 ref
vue
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} -> ${newVal}`)
})
count.value++ // 输出: count: 0 -> 1
</script>
2. 侦听 reactive 对象
⚠️ 注意 :侦听 reactive 对象时,必须使用 getter 函数 ,否则会隐式创建深度侦听,且新值等于旧值。
js
import { reactive, watch } from 'vue'
const state = reactive({ name: 'Tom', age: 18 })
// ❌ 错误:直接传 reactive,新旧值相同
watch(state, (newVal, oldVal) => {
console.log(newVal === oldVal) // true(因为都是同一个代理对象)
})
// ✅ 正确:使用 getter
watch(
() => state.name,
(newVal, oldVal) => {
console.log(`name: ${oldVal} -> ${newVal}`)
}
)
// ✅ 侦听整个 reactive 需要 deep
watch(
() => state,
(newVal) => console.log(newVal),
{ deep: true }
)
3. 侦听 getter 函数
js
const user = reactive({ first: '张', last: '三' })
// 侦听组合值
watch(
() => user.first + user.last,
(newVal, oldVal) => {
console.log(`全名: ${oldVal} -> ${newVal}`)
}
)
4. 侦听多个源(数组)
js
const a = ref(1)
const b = ref(2)
watch([a, b], ([newA, newB], [oldA, oldB]) => {
console.log(`a: ${oldA}->${newA}, b: ${oldB}->${newB}`)
})
// 混合写法
watch([a, () => b.value * 2], ([newA, newDoubleB]) => {
console.log(newA, newDoubleB)
})
三、核心配置项 options
1. immediate ------ 立即执行
js
const id = ref(1)
watch(
id,
(newVal) => {
console.log('立即执行一次:', newVal) // 首次运行就输出 1
},
{ immediate: true }
)
常用于组件初始化时根据 props 拉取数据。
2. deep ------ 深度侦听
js
const obj = ref({ nested: { count: 0 } })
// ❌ 不加 deep,修改嵌套属性不会触发
watch(obj, () => console.log('触发'))
obj.value.nested.count++ // 不会触发
// ✅ 加 deep
watch(
obj,
() => console.log('触发'),
{ deep: true }
)
obj.value.nested.count++ // 触发
reactive 对象的特殊情况:直接侦听 reactive 对象时,Vue 会自动启用 deep:
js
const state = reactive({ nested: { count: 0 } })
watch(state, () => {
console.log('自动深度侦听')
})
state.nested.count++ // 触发
性能优化:使用 getter 精确侦听,避免 deep 全量遍历。
js
// ✅ 只侦听特定属性,性能更好
watch(() => obj.value.nested.count, () => {})
3. flush ------ 回调执行时机
| 值 | 说明 |
|---|---|
'pre'(默认) |
组件更新前执行 |
'post' |
组件更新后执行 |
'sync' |
同步执行,变化立即触发 |
js
watch(source, cb, { flush: 'post' }) // 可访问更新后的 DOM
watch(source, cb, { flush: 'sync' }) // 谨慎使用,性能开销大
4. once ------ 只执行一次(Vue 3.4+)
js
watch(
source,
(newVal) => {
console.log('只执行一次') // 变化后只触发一次
},
{ once: true }
)
5. onCleanup ------ 清理副作用
用于取消上一次的异步请求、定时器等,防止竞态问题。
js
watch(id, async (newId, oldId, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort()) // 下次触发前先取消上次请求
const res = await fetch(`/api/user/${newId}`, {
signal: controller.signal
})
const data = await res.json()
console.log(data)
})
四、停止侦听
watch 返回一个停止函数,调用它可手动停止侦听。
js
const stop = watch(count, () => {
console.log('count 变化了')
})
count.value++ // 触发
stop() // 停止侦听
count.value++ // 不再触发
注意 :在
setup()或<script setup>中创建的 watch,会自动绑定到组件实例,组件卸载时自动停止。只有在异步回调中创建的 watch 才需要手动停止。
js
// 异步中创建,需要手动清理
setTimeout(() => {
const stop = watch(source, cb)
onUnmounted(stop)
}, 1000)
五、watchEffect ------ 自动追踪
watchEffect 会立即执行一次 ,并自动收集回调中用到的响应式依赖。
js
const count = ref(0)
const name = ref('Tom')
watchEffect(() => {
// 自动追踪 count 和 name
console.log(`count=${count.value}, name=${name.value}`)
})
// 立即输出一次,之后任一变化都触发
watch vs watchEffect 对比
| 特性 | watch | watchEffect |
|---|---|---|
| 执行时机 | 默认惰性 | 立即执行 |
| 依赖收集 | 显式指定 | 自动收集 |
| 旧值 | 可获取 | 无法获取 |
| 适用场景 | 需新旧值对比、精确控制 | 简单副作用 |
watchEffect 的停止与清理
js
const stop = watchEffect((onCleanup) => {
const timer = setInterval(() => {}, 1000)
onCleanup(() => clearInterval(timer))
})
stop() // 停止
watchPostEffect 和 watchSyncEffect
js
import { watchPostEffect, watchSyncEffect } from 'vue'
// flush: 'post'
watchPostEffect(() => {
// DOM 更新后执行
})
// flush: 'sync'
watchSyncEffect(() => {
// 同步执行
})
六、实战案例
案例 1:搜索框防抖
vue
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const results = ref([])
let timer = null
watch(keyword, (newVal) => {
clearTimeout(timer)
timer = setTimeout(async () => {
if (!newVal) {
results.value = []
return
}
const res = await fetch(`/api/search?q=${newVal}`)
results.value = await res.json()
}, 300)
})
</script>
<template>
<input v-model="keyword" placeholder="搜索..." />
<ul>
<li v-for="item in results" :key="item.id">{{ item.name }}</li>
</ul>
</template>
案例 2:侦听 props 变化
vue
<script setup>
import { watch, toRefs } from 'vue'
const props = defineProps({
userId: Number
})
// ✅ 推荐:getter 写法
watch(
() => props.userId,
async (newId) => {
if (newId) {
await loadUser(newId)
}
},
{ immediate: true }
)
</script>
案例 3:多源侦听 + 深度侦听
js
const form = reactive({
name: '',
address: { city: '', street: '' }
})
watch(
[() => form.name, () => form.address.city],
([newName, newCity], [oldName, oldCity]) => {
console.log(`name: ${oldName}->${newName}`)
console.log(`city: ${oldCity}->${newCity}`)
},
{ immediate: true }
)
案例 4:避免竞态的异步请求
js
watch(
() => props.id,
async (newId, oldId, onCleanup) => {
let cancelled = false
onCleanup(() => (cancelled = true))
const data = await fetchData(newId)
if (!cancelled) {
// 确保只渲染最新请求的结果
result.value = data
}
},
{ immediate: true }
)
七、常见坑点总结
- 侦听 reactive 对象直接传参会失效:必须用 getter,否则新旧值相同。
- ref 对象的
.value:侦听 ref 本身传ref,侦听属性传() => ref.value.xxx。 - deep 性能问题:大对象深度侦听开销大,优先用 getter 精确侦听。
- 数组/对象引用变化 :
watch侦听的是引用,直接修改属性需deep: true。 - 异步创建的 watch 不会自动清理 :需要手动调用停止函数,或用
onUnmounted清理。 watchEffect无法获取旧值 :需要新旧值对比时用watch。flush: 'sync'慎用:频繁触发会导致性能问题。
八、速查表
| 场景 | 写法 |
|---|---|
| 侦听单个 ref | watch(ref, cb) |
| 侦听 reactive 属性 | watch(() => obj.key, cb) |
| 侦听整个 reactive | watch(obj, cb)(自动 deep) |
| 侦听多个源 | watch([a, b], cb) |
| 立即执行 | { immediate: true } |
| 深度侦听 | { deep: true } |
| DOM 更新后执行 | { flush: 'post' } |
| 只执行一次 | { once: true } |
| 自动追踪依赖 | watchEffect(fn) |
| 停止侦听 | const stop = watch(...); stop() |
掌握以上内容,即可应对 Vue 3 中绝大多数 watch 使用场景。核心原则:能精确侦听就不用 deep,能用 getter 就不传对象,异步副作用记得清理。