在 Vue 3 中,watch() 是组合式 API 的重要成员,用于响应式地监听数据变化并触发副作用。虽然我们大多数人都用过它,但你是否真正了解它的高阶用法?是否遇到过这些问题:
-
为什么 watch 的回调会在组件挂载时就触发?
-
如何深度监听一个嵌套对象的变化?
-
如何同时监听多个数据源?
-
如何控制监听回调的时机?
-
如何手动停止监听?
这篇文章将带你由浅入深地掌握 watch() 的高级用法,彻底告别"用得了但搞不清"。
🧩 基础回顾:watch 是干嘛的?
watch 就是用来侦听响应式数据的变化,当被监听的值变化时,执行副作用逻辑。
js
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
当你运行 count.value++ 时,会触发 watch 的回调函数。
📘 一、监听多个数据源(数组监听)
有时你需要同时监听多个变量,使用数组即可:
js
const a = ref(1)
const b = ref(2)
watch([a, b], ([newA, newB], [oldA, oldB]) => {
console.log(`a: ${oldA} → ${newA}`)
console.log(`b: ${oldB} → ${newB}`)
})
注意:监听数组时,参数是数组结构的 [newValArray, oldValArray] ,不是单个值。
🔍 二、深度监听对象(deep watch)
当监听对象或数组时,默认是浅监听,只监听引用变化:
js
const obj = ref({ name: 'Vue', age: 3 })
watch(obj, (newVal) => {
console.log('对象整体变了')
})
如果只是 obj.value.age++,不会触发 watch。解决办法是开启 deep: true:
js
watch(obj, (newVal) => {
console.log('深度监听:属性变了', newVal)
}, { deep: true })
注意:监听响应式对象 **最好不要监听 ref(obj) 而是监听 obj.value 内部字段或使用 reactive() 结合 deep。
🕹 三、控制监听回调触发时机(immediate)
默认情况下,watch 只在依赖变化时触发。使用 immediate: true 可以在初次监听时就执行一次回调:
js
watch(count, (newVal, oldVal) => {
console.log('首次立即执行', newVal)
}, { immediate: true })
常用于:
- 初始化时拉取接口数据;
- 初始化日志打印;
- 需要拿到默认值执行逻辑。
⏰ 四、监听对象某个字段变化(函数监听)
你可以监听一个 getter 函数,精准指定你要监听的响应式属性:
js
const user = ref({ name: 'Alice', age: 25 })
watch(() => user.value.age, (newAge) => {
console.log('年龄变了', newAge)
})
这样可以避免 deep 带来的性能消耗,非常推荐在实际开发中使用。
-
- *js
🔄 五、监听 reactive 对象的任意字段变化
如果你用的是 reactive(),无法直接 watch 某个属性。你有两种选择:
✅ 方法一:使用 getter
js
const user = reactive({ name: 'Bob', age: 30 })
watch(() => user.age, (newVal) => {
console.log('age 改变了', newVal)
})
✅ 方法二:直接 deep 监听整个对象
javascript
watch(user, (newVal) => {
console.log('任意字段变了', newVal)
}, { deep: true })
但注意:deep 有性能开销,尽量精确监听具体字段。
🧼 六、watch 的清理函数(副作用释放)
当你在 watch 内部使用异步操作或副作用逻辑,比如定时器或网络请求,需要在下次触发之前清理:
js
watch(count, (newVal, oldVal, onCleanup) => {
const timer = setTimeout(() => {
console.log('模拟异步请求完成')
}, 1000)
onCleanup(() => {
clearTimeout(timer)
console.log('上一次副作用被清除')
})
})
这可以防止旧请求/副作用残留,避免竞态条件、内存泄漏等问题。
❌ 七、如何停止监听?
watch 会返回一个停止监听的函数,你可以在某些条件下主动调用:
js
const stop = watch(count, (val) => {
console.log('监听中', val)
if (val > 5) {
stop()
console.log('监听已停止')
}
})
🧠 八、最佳实践建议
- ✅ 精准监听字段(使用 getter)而非整个对象;
- ✅ deep: true 慎用,有性能开销;
- ✅ 需要初始化调用就加 immediate: true;
- ✅ 有异步副作用时配合 onCleanup 清理;
- ✅ 大多数时候优先使用 computed,watch 用于"有副作用"的场景。
🧪 九、实战演练:用户表单自动保存
js
const form = reactive({
name: '',
email: ''
})
watch(
() => ({ ...form }), // 简单实现浅层结构监听
(val) => {
console.log('自动保存', val)
saveToLocalStorage(val)
},
{ deep: true, immediate: true }
)
function saveToLocalStorage(data) {
localStorage.setItem('form-data', JSON.stringify(data))
}
🎯 总结
场景 | 推荐做法 |
---|---|
监听单个 ref | 直接传变量 |
监听多个数据源 | 传数组 |
监听某个字段 | 传函数 |
初始即执行 | { immediate: true } |
对象内部变动 | { deep: true } 或监听字段 |
清理副作用 | 使用 onCleanup |
手动停止 | 调用 stop() |
🪄 最后
watch() 是 Vue 响应式系统中不可或缺的一部分,很多场景都离不开它,比如接口请求、缓存逻辑、UI 控制等。希望这篇文章能帮你彻底掌握 watch 的各种高阶用法,写出更健壮、更高效的 Vue 组件。
如果觉得这篇文章对你有帮助,欢迎点赞 + 收藏 + 转发,一起成长 💪