Vue.js: 侦听器 watch
最后更新:2026-08-26
侦听器(watch / watchEffect)让你监听响应式数据的变化并在变化时执行副作用。computed 用于"派生新值",watch 用于"触发动作"(如发请求、设状态)。
watch 是 Vue 响应式系统的重要组成部分,3 个核心场景:数据变化发 API、表单输入防抖、跨数据同步。
1. 你将学到
watch()基本语法和 4 个参数(source / callback / options / deep)- 监听 ref / reactive / computed / getter 函数 4 种数据源
watchEffect()自动追踪依赖与 watch 的区别immediate: true立即触发选项deep: true深度监听内部属性- 防抖节流实现
- 清理副作用(onCleanup / 组件卸载)
2. 一个搜索框的 API 性能问题
(2) 痛点:每个键入都发请求,API 崩溃
Alice 在后台实现了一个商品搜索框:
JS
// ❌ 翻车版:每个字符都发请求
const { ref } = Vue
const searchQuery = ref('')
watch(searchQuery, async (newValue) => {
// 每次输入都发请求
const results = await fetch(`/api/search?q=${newValue}`)
searchResults.value = await results.json()
})
API 服务器崩溃了:
- 用户输入"iPhone 15"(8 个字符)= 8 次 API 调用
- 100 个用户同时输入 = 800 次 API 调用
- API 限流:100 次/分钟 → 服务器崩溃
后端开发者 Charlie 报告:
"Alice,你的搜索在疯狂轰炸 API。我们需要防抖。等用户停止输入 300ms 后再发请求。"
(2) Vue watch + 防抖解法
JS
const { ref, watch } = Vue
const searchQuery = ref('')
const searchResults = ref([])
// 防抖:300ms 内不变化才发请求
let timeoutId = null
watch(searchQuery, (newValue) => {
if (timeoutId) clearTimeout(timeoutId)
timeoutId = setTimeout(async () => {
const response = await fetch(`/api/search?q=${newValue}`)
searchResults.value = await response.json()
}, 300)
})
(3) 收益
实现防抖后:
- API 调用:每次搜索 8 次 → 1 次(-87%)
- 服务器负载:800 → 100(-87%)
- 响应时间:50ms(缓存)vs 500ms(实时)
- 服务器稳定性:99.9%(不再崩溃)
3. watch() 基本语法
(1) 4 个参数
JS
const { ref, watch } = Vue
const count = ref(0)
watch(
// 1. 监听源:要监听的响应式数据
count,
// 2. 回调函数:(新值, 旧值) => {...}
(newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
},
// 3. 选项(可选):immediate, deep, flush
{ immediate: true, deep: true }
)
(2) 监听 ref
JS
const count = ref(0)
// 监听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
(3) 监听多个 ref
JS
const firstName = ref('Alice')
const lastName = ref('Smith')
// 监听多个
watch([firstName, lastName], ([newFirst, newLast], [oldFirst, oldLast]) => {
console.log(`Name: ${newFirst} ${newLast}`)
})
4. 4 种监听源
(1) 监听 ref
JS
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
(2) 监听 reactive 对象
JS
const { reactive, watch } = Vue
const user = reactive({ name: 'Alice', age: 25 })
// 监听整个 reactive(自动深度)
watch(user, (newVal, oldVal) => {
console.log('User changed:', newVal)
})
// ✅ 修改内部属性也触发
user.name = 'Bob' // 触发
(3) 监听 getter 函数
JS
const state = reactive({ user: { profile: { name: 'Alice' } } })
// 只监听特定属性(精确控制)
watch(
() => state.user.profile.name,
(newVal) => {
console.log(`Name: ${newVal}`)
}
)
(4) 监听 computed
JS
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
watch(doubleCount, (newVal) => {
console.log(`Double: ${newVal}`)
})
(4) 4 种监听源对比
| 数据源 | 写法 | 是否深度 | 适用 |
|---|---|---|---|
| ref | watch(ref, fn) |
默认浅 | 单个 ref |
| reactive | watch(reactive, fn) |
自动深 | 对象 |
| getter | watch(() => obj.x, fn) |
默认浅 | 特定属性 |
| computed | watch(computed, fn) |
默认浅 | 派生值 |
5. watchEffect():自动追踪依赖
(1) 基本语法
JS
const { ref, watchEffect } = Vue
const count = ref(0)
const name = ref('Alice')
// 不需要指定监听谁,函数体里用到的响应式数据自动追踪
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
// count 变化时重新执行
// name 变化时也重新执行
})
(2) watchEffect vs watch
| 维度 | watch | watchEffect |
|---|---|---|
| 监听源 | 手动指定 | 自动追踪 |
| 立即执行 | 默认否(需 immediate: true) |
✅ 是 |
| 新旧值 | ✅ 都有 | ❌ 只有新值 |
| 清理副作用 | onCleanup | onCleanup |
| 适用场景 | 精确监听 | 通用副作用 |
(3) watchEffect 的清理
JS
const { watchEffect, onCleanup } = Vue
watchEffect((onCleanup) => {
// 设置定时器、订阅、事件监听
const timer = setInterval(() => {
console.log('tick')
}, 1000)
// 清理:下次执行前或组件卸载时调用
onCleanup(() => {
clearInterval(timer)
})
})
6. 选项:immediate / deep / flush
(1) immediate:立即触发
JS
const count = ref(0)
// 默认:count 变化才触发
watch(count, (newVal) => console.log(newVal)) // 不会立即执行
// immediate: true:立即触发 1 次
watch(count, (newVal) => console.log(newVal), { immediate: true })
// ✅ 立即执行:0 → 0
(2) deep:深度监听
JS
const { ref, watch } = Vue
const user = ref({ profile: { name: 'Alice' } })
// 默认:ref 包装对象的 .value 整体替换才触发
watch(user, () => console.log('changed')) // ❌ user.value.profile.name = 'Bob' 不触发
// deep: true:内部属性变化也触发
watch(user, () => console.log('changed'), { deep: true })
// ✅ user.value.profile.name = 'Bob' 触发
(3) flush:触发时机
JS
// 默认 'pre':组件更新前触发
watch(source, fn, { flush: 'pre' })
// 'post':组件更新后触发(DOM 已更新)
watch(source, fn, { flush: 'post' })
// 'sync':同步触发(不常用,性能差)
watch(source, fn, { flush: 'sync' })
7. 完整示例:搜索框 + 防抖
▶ 示例:基础 watch 监听 ref
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<input type="number" v-model.number="count">
<p>Count: {{ count }}</p>
<p>查看 console 看变化日志</p>
</div>
<script>
const { createApp, ref, watch } = Vue
createApp({
setup() {
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`Count changed: ${oldVal} → ${newVal}`)
})
return { count }
}
}).mount('#app')
</script>
▶ 示例:防抖搜索(CDN 版模拟)
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<input v-model="searchQuery" placeholder="Search products...">
<p>Recent query: {{ lastQuery || '(空)' }}</p>
<p>触发次数: {{ triggerCount }}</p>
</div>
<script>
const { createApp, ref, watch } = Vue
createApp({
setup() {
const searchQuery = ref('')
const lastQuery = ref('')
const triggerCount = ref(0)
let timeoutId = null
watch(searchQuery, (newQuery) => {
// 清除上一次的定时器
if (timeoutId) clearTimeout(timeoutId)
// 设置新的定时器:300ms 后才执行(防抖)
timeoutId = setTimeout(() => {
lastQuery.value = newQuery
triggerCount.value++
}, 300)
})
return { searchQuery, lastQuery, triggerCount }
}
}).mount('#app')
</script>
注:真实项目用 fetch + 后端 API,这里用 setTimeout 模拟触发逻辑。
▶ 示例:监听 reactive 深度变化
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>姓名: {{ user.profile.name }}</p>
<p>年龄: {{ user.profile.age }}</p>
<p>主题: {{ user.settings.theme }}</p>
<p>变更计数: {{ changeCount }}</p>
<button @click="user.profile.name = 'Bob'">改名 Bob</button>
<button @click="user.settings.theme = 'light'">切换 light</button>
</div>
<script>
const { createApp, reactive, ref, watch } = Vue
createApp({
setup() {
const user = reactive({
profile: { name: 'Alice', age: 25 },
settings: { theme: 'dark' }
})
const changeCount = ref(0)
// 监听整个 user(reactive 自动深度)
watch(user, () => { changeCount.value++ })
return { user, changeCount }
}
}).mount('#app')
</script>
▶ 示例:watchEffect 自动追踪
HTML
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<div id="app">
<p>count: {{ count }}</p>
<p>name: {{ name }}</p>
<p>effect 触发次数: {{ effectCount }}</p>
<button @click="count++">count++</button>
<button @click="name = 'Bob'">改名 Bob</button>
</div>
<script>
const { createApp, ref, watchEffect } = Vue
createApp({
setup() {
const count = ref(0)
const name = ref('Alice')
const effectCount = ref(0)
// 不指定监听源,函数体用到的自动追踪
watchEffect(() => {
console.log(`count: ${count.value}, name: ${name.value}`)
effectCount.value++ // 立即执行 + 依赖变化时再执行
})
return { count, name, effectCount }
}
}).mount('#app')
</script>
▶ 示例:5 个常见错误速查
| 错误 | 现象 | 解决 |
|---|---|---|
| 监听 reactive 不设 deep | 内部属性变化不触发 | watch(obj, fn, { deep: true }) |
| 监听 ref 包装对象整体 | 内部属性变化不触发 | 改用 reactive 或加 deep |
| watchEffect 写无限循环 | 浏览器卡死 | 避免修改函数体内用到的数据 |
| 没清理定时器 | 内存泄漏 | 用 onCleanup |
| 立即执行混淆 | 想用旧值但没拿到 | 用 immediate + oldVal(可能 undefined) |
❓ 常见问题
Q watch 和 watchEffect 选哪个?
A 能用 watch 选 watch(精确控制 + 新旧值)。watchEffect 适合"我要在某个数据变化时做某事,但不想列出全部依赖"的场景。性能差不多,watchEffect 略慢(要追踪函数体依赖)。
Q watch 监听 ref 时,新旧值是什么类型?
A
.value。如果 ref 包装对象,新旧值都是 ref.value(引用相同,除非整个替换)。要监听内部属性变化,用 reactive 或 getter 函数。Q watch 监听 ref 包装对象,怎么监听内部属性?
A 3 种方式:(1)
watch(ref, fn, { deep: true });(2) watch(() => ref.value.x, fn)(getter);(3) 改用 reactive 包装。Q watch 能监听 props 吗?
A 能。
watch(() => props.count, fn)。注意 props 是只读的,watch 回调中不要修改 props。Q immediate: true 时 oldVal 是什么?
A
undefined(首次触发没有"旧值")。可以从 ref 初始值或 store 拿旧值。Q watch 能异步吗?
A 回调函数可以是 async,但 watch 本身是同步触发的。异步逻辑放在回调里:
(newVal) => { setTimeout(() => {...}, 0) }。Q onCleanup 什么时候触发?
A 2 个时机:(1) watch 重新执行前(上一次清理);(2) 组件卸载时。适合清理定时器、取消请求、移除事件监听。
📖 小节
- watch 监听响应式数据变化,4 个参数:source / callback / options / deep
- 4 种监听源:ref / reactive / getter 函数 / computed
- watchEffect 自动追踪函数体内的响应式依赖,立即执行 1 次
- immediate: true 立即触发,deep: true 深度监听内部属性
- 防抖:300ms 内最后一次触发才执行;节流:1 秒内最多 1 次
- onCleanup 清理副作用:定时器、订阅、事件监听
- watch vs watchEffect:watch 精确控制 + 新旧值,watchEffect 自动追踪 + 简单
📝 作业
-
基础题(难度⭐) 用 watch 监听一个 ref 变量
count:- count 变化时打印 "count: ${old} → ${new}"
- 用
immediate: true让回调立即执行 1 次
-
进阶题(难度⭐⭐) 用 watch 实现一个"自动保存"功能:
- 编辑一个 ref 文本
content - 1 秒后自动调用
save(content)函数(防抖) - 切换到其他页面时(组件卸载)清理定时器
- 编辑一个 ref 文本
-
挑战题(难度⭐⭐⭐) 实现一个完整的搜索 + 自动完成 + 历史记录系统:
- 防抖搜索(300ms)
- 显示搜索结果列表(v-for)
- 保留最近 5 次搜索历史
- 点击历史项可重新搜索
- 切换搜索词时取消未完成的请求
- 用 watchEffect + onCleanup 实现