侦听器
本教程共 40 篇 · 第 13 篇 · 更新于 2026-07-29 · 约 8 分钟阅读
13. 侦听器
本节目标:学会用 watch 和 watchEffect 在状态变化时执行副作用,掌握深层侦听和立即侦听。
基础用法
计算属性适合声明派生状态。但有些场景需要在状态变化时执行”副作用”——比如操作 DOM、发请求、改其他状态。
Composition API 用 watch 函数:
<script setup>
import { ref, watch } from 'vue'
const question = ref('')
const answer = ref('问题通常包含问号。;-)')
const loading = ref(false)
watch(question, async (newQuestion, oldQuestion) => {
if (newQuestion.includes('?')) {
loading.value = true
answer.value = '思考中...'
try {
const res = await fetch('https://yesno.wtf/api')
answer.value = (await res.json()).answer
} catch (error) {
answer.value = '错误!无法访问 API。' + error
} finally {
loading.value = false
}
}
})
</script>
watch 第一个参数是响应式源,第二个是回调。源变化时回调执行,回调接收新值和旧值。
侦听源类型
watch 第一个参数可以是:
const x = ref(0)
const y = ref(0)
// 单个 ref
watch(x, (newX) => {
console.log(`x 是 ${newX}`)
})
// getter 函数
watch(
() => x.value + y.value,
(sum) => {
console.log(`x + y 的和是 ${sum}`)
}
)
// 多个源的数组
watch([x, () => y.value], ([newX, newY]) => {
console.log(`x 是 ${newX},y 是 ${newY}`)
})
Warning不能直接侦听 reactive 对象的属性:
const obj = reactive({ count: 0 })
// 不行!传的是数字给 watch
watch(obj.count, (count) => {
console.log(`Count is: ${count}`)
})
要用 getter:
watch(
() => obj.count,
(count) => {
console.log(`Count is: ${count}`)
}
)
深层侦听
watch 默认是浅层的——只有被侦听的属性被赋新值时才触发,嵌套属性变化不触发。
直接侦听 reactive 对象会隐式创建深层侦听器:
const obj = reactive({ count: 0 })
watch(obj, (newValue, oldValue) => {
// 嵌套属性变化也会触发
})
obj.count++
如果侦听的是返回 reactive 对象的 getter,只有 getter 返回不同对象时才触发:
watch(
() => state.someObject,
() => {
// 只有 state.someObject 被替换时才触发
}
)
想强制深层侦听,用 deep 选项:
watch(
() => state.someObject,
(newValue, oldValue) => { /* ... */ },
{ deep: true }
)
Warning深层侦听要遍历对象的所有嵌套属性,大数据结构上开销大。只在必要时用,注意性能。
立即侦听
watch 默认是懒的——源变了才回调。想创建时就执行一次,用 immediate: true:
watch(
source,
(newValue, oldValue) => {
// 创建时立即执行,之后 source 变化再执行
},
{ immediate: true }
)
watchEffect
侦听器的回调经常用和源一样的响应式状态。比如:
const todoId = ref(1)
const data = ref(null)
watch(todoId, async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
}, { immediate: true })
todoId 用了两次——一次做源,一次在回调里。watchEffect 可以简化:
watchEffect(async () => {
const response = await fetch(
`https://jsonplaceholder.typicode.com/todos/${todoId.value}`
)
data.value = await response.json()
})
回调立即执行,期间自动追踪 todoId.value 作为依赖。todoId.value 变了就重新执行。不需要显式传源。
Tip
watchEffect只在同步执行期间追踪依赖。异步回调里,只有第一个await之前访问的属性才会被追踪。
watch vs watchEffect
| watch | watchEffect | |
|---|---|---|
| 依赖追踪 | 只追踪显式源 | 自动追踪回调里所有响应式属性 |
| 触发时机 | 源变化才触发 | 创建时立即执行 + 依赖变化触发 |
| 控制粒度 | 更精确 | 更方便但不够显式 |
副作用清理
侦听器里发请求时,如果源在请求完成前变了,旧请求的结果就过时了。理想情况是源变化时取消过时的请求。
用 onWatcherCleanup()(3.5+)注册清理函数:
import { watch, onWatcherCleanup } from 'vue'
watch(id, (newId) => {
const controller = new AbortController()
fetch(`/api/${newId}`, { signal: controller.signal }).then(() => {
// 处理响应
})
onWatcherCleanup(() => {
controller.abort() // 取消过时请求
})
})
Note
onWatcherCleanup必须在watch回调或watchEffect函数的同步执行期间调用,不能在await之后。
也可以用回调的第三个参数 onCleanup:
watch(id, (newId, oldId, onCleanup) => {
// ...
onCleanup(() => {
// 清理逻辑
})
})
停止侦听器
setup() 或 <script setup> 里同步创建的侦听器会自动绑定到组件实例,组件卸载时自动停止。
需要手动停止时,调用返回的函数:
const unwatch = watchEffect(() => {})
// 不再需要时
unwatch()
Important异步创建的侦听器不会自动停止,必须手动停止避免内存泄漏。尽量同步创建。
本节回顾
watch侦听响应式源,变化时执行回调- 源可以是 ref、getter、reactive 对象或多个源的数组
- 深层侦听用
deep选项,注意性能 immediate: true创建时立即执行watchEffect自动追踪依赖,立即执行onWatcherCleanup清理过时副作用- 同步创建的侦听器自动停止