一、前言
上篇 computed 管"算值",但它禁止干杂活。实际开发中大量需求是"数据一变,就去做某件事":搜索词一变就发请求、配置一变就存 localStorage、路由一变就刷新列表。这些"做事"的场景,主角就是 watch 侦听器。
二、基础用法:监听一个 ref
watch(要监听的数据, (新值, 旧值) => { 做事 }):
vue
<template>
<p>计数:{{ count }}</p>
<button @click="count++">+1</button>
</template>
<script setup>
import { ref, watch } from 'vue'
const count = ref(0)
// 数据一变,回调立刻执行
watch(count, (newVal, oldVal) => {
console.log(`从 ${oldVal} 变成了 ${newVal}`)
// 这里可以:发请求、存本地、弹提示......干一切"副作用"
})
</script>
点三次按钮,控制台依次输出:从 0 变成了 1、从 1 变成了 2、从 2 变成了 3。
注意和 computed 的分工:watch 的回调不需要 return 值,它不为页面提供数据,纯粹是"听到变化后去执行的动作"。
三、监听的正确姿势(新手最容易写错的地方)
1. 监听 ref 包的基本类型:直接传名字
js
watch(count, cb) // ✅
watch(count.value, cb) // ❌ 传了个死值 0,永远监听不到变化
2. 监听 reactive 对象的单个属性:必须用 getter 函数
js
const user = reactive({ name: '小明', age: 18 })
watch(() => user.age, (n, o) => {
console.log(`年龄 ${o} → ${n}`)
}) // ✅ 传一个"返回该属性"的箭头函数
watch(user.age, cb) // ❌ 传的是 18 这个死值,还伴随警告
3. 监听整个 reactive 对象:直接传,自动深度监听
js
watch(user, (n) => {
console.log('user 里有任何属性变了', n)
}, { deep: true }) // 传 reactive 对象时 deep 写不写都深层监听
4. 监听 ref 包的对象:改内部属性默认不触发,要加 deep
js
const data = ref({ list: [1, 2] })
watch(data, cb) // 改 data.value.list 只能听个寂寞
watch(data, cb, { deep: true }) // ✅ 深层属性变化也能听到
四、多数据源与两个常用选项
同时监听多个数据,源写成数组,回调参数也是数组:
js
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log('姓或名变了', newFirst, newLast)
})
immediate:进页面立刻执行一次
默认数据"变了才执行",但有的场景初始化就要跑一次(比如进页面先按默认关键词查一次):
js
watch(keyword, fetchData, { immediate: true }) // 创建时先执行一次
deep:深度监听
上面第三、四节的场景。提醒:deep 会递归遍历所有层级,超大对象慎用,性能篇会给替代方案。
五、综合实战:搜索联想(发请求版)
真实项目里 watch 出镜率最高的场景------输入关键词请求接口:
vue
<template>
<input v-model="keyword" placeholder="输入关键词搜索" />
<p v-if="loading">🔍 搜索中...</p>
<ul v-else>
<li v-for="item in result" :key="item">{{ item }}</li>
</ul>
</template>
<script setup>
import { ref, watch } from 'vue'
const keyword = ref('')
const loading = ref(false)
const result = ref([])
// 模拟的接口
function fetchSearch(kw) {
return new Promise(resolve => {
setTimeout(() => {
resolve(['Vue3 ' + kw, 'Vite ' + kw, kw + ' 教程'])
}, 500)
})
}
watch(keyword, async (newKw) => {
if (!newKw) {
result.value = []
return
}
loading.value = true
result.value = await fetchSearch(newKw)
loading.value = false
})
</script>
每敲一个字自动重新搜索,loading 状态自动切换------数据驱动的搜索,一行 DOM 操作都没写。
【截图位置:输入"教程"后搜索中与结果列表的运行效果】
小瑕疵留给你思考:连续快速输入会发一堆请求,怎么只发最后一次?(防抖,012 篇和性能篇都会再遇到它)
六、踩坑记录
- watch(count.value, cb) :传了死值,监听无效------记住"传引用不传值",对象属性用 getter 函数
() => obj.x - 监听整个 reactive 时新旧值相同 :
newVal === oldVal,因为是同一个对象引用。要拿到"哪变了",监听具体属性 - 回调里又改了被监听的数据:无限循环警告"Maximum recursive updates exceeded"------回调里别改自己正在监听的
- 进页面不执行 :想要初始化也执行一次,
immediate: true - 组件卸载忘停止 :在 setup 里写的 watch 会随组件卸载自动停止,放心用;但如果是在 setTimeout/手动回调里创建的 watch,必须手动接住返回值
const stop = watch(...),需要时stop()
七、今日小结
- watch = 监听数据变化,执行副作用(发请求/存本地/弹提示)
- 四种监听姿势:ref 直接传、对象属性用 getter、reactive 自动深层、ref 包对象要 deep
immediate初始化即执行,deep深层监听,多数据源传数组- computed 算值有缓存,watch 做事没缓存,各司其职
作者:陈志宝
下篇预告
watch 还要手动指定"听谁",有个更懒的兄弟 watchEffect:回调里用到谁就自动听谁,还自带"立即执行"。下一篇讲透它与 watch 的区别和分工。