watch / watchEffect 侦听器
watch和watchEffect用于在响应式状态变化时执行副作用——API 请求、DOM 操作、持久化等。watch显式指定侦听源、懒执行、可获取旧值;watchEffect自动追踪依赖、立即执行、无需手动声明源。
速查卡片
| 特性 | watch | watchEffect |
|---|---|---|
| 依赖追踪 | 显式指定源 | 自动收集 |
| 首次执行 | 否(懒) | 是(立即) |
| 获取旧值 | ✅ (newVal, oldVal) | ❌ |
| 多个源 | ✅ 传入数组 | 自动追踪所有读取的依赖 |
| 深度侦听 | { deep: true } | 自动(副作用内访问即追踪) |
| 副作用清理 | onCleanup 参数 / onWatcherCleanup(3.5+) | onCleanup 参数 / onWatcherCleanup(3.5+) |
| 暂停/恢复 | ✅ pause() / resume() | ✅ pause() / resume() |
| 一次性 | ✅ { once: true }(3.4+) | ❌(可用 watch 替代) |
| 调试钩子 | onTrack / onTrigger | onTrack / onTrigger |
| 版本 | ≥ 3.0.0 | ≥ 3.0.0 |
| 场景 | 推荐 |
|---|---|
| 根据 ID 变化请求数据 | watch(id, async (newId) => ...) |
| 自动收集依赖并执行副作用 | watchEffect |
| 需要旧值对比 | watch |
| 操作更新后的 DOM | watch + { flush: 'post' } 或 watchPostEffect |
| 简单派生新值 | computed(不用侦听器) |
概述
computed 用于产生一个值,侦听器用于做一件事。当状态变化时需要执行 API 请求、操作 DOM、写 localStorage 等副作用时,用侦听器。
关于
computed的完整用法及与侦听器的区别,详见 computed 计算属性。
两个核心 API:
watch:显式指定侦听源,回调在源变化时触发。懒执行(默认不立即触发),可获取新旧值。watchEffect:自动追踪副作用函数内访问的所有响应式依赖。立即执行一次以建立追踪,之后依赖变化自动重新执行。
watch() 详解
侦听源类型
watch() 的第一个参数可以是:
| 源类型 | 示例 | 说明 |
|---|---|---|
ref | watch(count, ...) | 侦听 ref 的 .value 变化 |
getter 函数 | watch(() => state.x, ...) | 返回要侦听的值,精确控制 |
| 响应式对象(reactive) | watch(state, ...) | 自动开启隐式 deep |
| 以上类型的数组 | watch([ref1, getter2], ...) | 同时侦听多个源 |
import { ref, reactive, watch } from 'vue'
const count = ref(0)
const state = reactive({ name: 'Vue', age: 3 })
// 侦听 ref
watch(count, (newVal, oldVal) => {
console.log(`count: ${oldVal} → ${newVal}`)
})
// 侦听 getter
watch(
() => state.name,
(newName, oldName) => console.log(`name: ${oldName} → ${newName}`)
)
// 侦听 reactive 对象(自动 deep)
watch(state, (newState, oldState) => {
// 注意:deep 模式下 newState === oldState(同一引用)
console.log('state 变化了')
})
// 多源侦听
watch(
[count, () => state.name],
([newCount, newName], [oldCount, oldName]) => {
console.log(`count: ${oldCount}→${newCount}, name: ${oldName}→${newName}`)
}
)回调参数
type WatchCallback<T> = (
value: T, // 新值
oldValue: T, // 旧值(首次为 undefined)
onCleanup: (fn: () => void) => void // 注册清理函数
) => void侦听 reactive 对象的隐式 deep
当你直接传一个 reactive 对象作为源时,Vue 自动启用深度侦听——对象任意嵌套属性的变化都会触发回调:
const state = reactive({ user: { name: 'Zhang', profile: { avatar: '' } } })
// 自动 deep:修改深层属性会触发
watch(state, (newState) => {
console.log('state 变化') // state.user.profile.avatar 变化也触发
})
// 但如果用 getter 返回对象属性,则不会自动 deep:
watch(
() => state.user,
(user) => console.log(user) // 仅 state.user 引用变化才触发
)关键差异:
watch(reactiveObj, cb)自动 deep;watch(refObj, cb)不自动 deep(即使 ref 的值是对象,ref 本身的变化只检测.value引用变化)。
watchEffect() 详解
自动追踪 + 立即执行
import { ref, computed, watchEffect } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
watchEffect(() => {
// 立即执行一次;读取 count.value → 自动追踪
console.log(`count 是 ${count.value}`)
// 之后再读取 computed —— 若 computed 依赖 count,也会自动追踪
console.log(`双倍是 ${doubled.value}`)
})watchEffect 不需要显式声明侦听源——回调内实际读取过的响应式数据都会被自动追踪。
副作用清理(onCleanup)
watchEffect 的回调接收一个 onCleanup 函数,用于注册清理逻辑。每当副作用重新执行前,上一次的清理函数会被调用:
watchEffect((onCleanup) => {
const source = new EventSource('/api/events')
onCleanup(() => {
source.close() // 下次执行前自动关闭上一次的连接
})
})返回值:停止侦听
const stop = watchEffect(() => { /* ... */ })
// 手动停止
stop()
// 组件卸载时 watchEffect 也会自动停止(在 setup / <script setup> 中)watchPostEffect / watchSyncEffect
Vue 3.2+ 提供两个别名,等价于 watchEffect + 指定 flush:
| API | 等价于 | 回调执行时机 |
|---|---|---|
watchPostEffect(fn) | watchEffect(fn, { flush: 'post' }) | 组件 DOM 更新之后 |
watchSyncEffect(fn) | watchEffect(fn, { flush: 'sync' }) | 响应式变化时同步触发 |
import { watchPostEffect, watchSyncEffect } from 'vue'
// 需要在回调中访问更新后的 DOM
watchPostEffect(() => {
console.log(el.value?.offsetHeight) // 拿到最新高度
})
// 需要同步响应(慎用,无批处理)
watchSyncEffect(() => {
// 每次依赖变化都同步执行,性能敏感场景慎用
})基础用法
示例一:最简——watch 监听 ref 变化
<script setup lang="ts">
import { ref, watch } from 'vue'
const count = ref(0)
watch(count, (newVal, oldVal) => {
console.log(`count 从 ${oldVal} 变成了 ${newVal}`)
})
</script>
<template>
<button @click="count++">{{ count }}</button>
</template>示例二:典型——搜索防抖 + API 请求(watch + onWatcherCleanup)
<script setup lang="ts">
import { ref, watch } from 'vue'
import { onWatcherCleanup } from 'vue'
const query = ref('')
const results = ref<string[]>([])
watch(query, async (newQuery) => {
// 注册清理:如果 query 在请求完成前再次变化,取消上一次请求
const controller = new AbortController()
onWatcherCleanup(() => controller.abort())
try {
const res = await fetch(`/api/search?q=${newQuery}`, {
signal: controller.signal
})
results.value = await res.json()
} catch (e) {
if (!(e instanceof DOMException && e.name === 'AbortError')) {
console.error(e)
}
}
})
</script>
<template>
<input v-model="query" placeholder="搜索..." />
<ul>
<li v-for="r in results" :key="r">{{ r }}</li>
</ul>
</template>示例三:进阶——多源 + immediate 初始化加载
<script setup lang="ts">
import { ref, watch } from 'vue'
const categoryId = ref(1)
const page = ref(1)
const data = ref<any[]>([])
watch(
[categoryId, page],
async ([newCat, newPage]) => {
console.log(`加载分类 ${newCat} 第 ${newPage} 页`)
data.value = await fetch(`/api/list?cat=${newCat}&page=${newPage}`)
.then(r => r.json())
},
{ immediate: true } // 页面初始即加载
)
</script>示例四:进阶——flush: 'post' 操作更新后的 DOM
<script setup lang="ts">
import { ref, watch } from 'vue'
const list = ref([1, 2, 3])
const containerRef = ref<HTMLElement | null>(null)
watch(
list,
() => {
// 现在能拿到更新后的滚动高度
console.log('新高度:', containerRef.value?.scrollHeight)
},
{ flush: 'post' } // DOM 更新后才执行
)
</script>
<template>
<div ref="containerRef">
<p v-for="i in list" :key="i">{{ i }}</p>
</div>
<button @click="list.push(list.length + 1)">添加</button>
</template>示例五:冷门——deep: number 限制遍历深度(3.5+)
import { ref, watch } from 'vue'
const bigData = ref({
level1: {
level2: {
level3: { value: 42 },
other: { value: 99 }
}
}
})
// 仅侦听 2 层深度——level3 的变化不会触发
watch(bigData, (val) => {
console.log('2 层以内变化')
}, { deep: 2 })
// 对比:deep: true 会递归追踪所有层级(大数据下性能开销大)
// 3.5+ 可以精确控制遍历深度,避免不必要的深层追踪示例六:冷门——once: true 一次性侦听(3.4+)
import { ref, watch } from 'vue'
const ready = ref(false)
// 只触发一次,之后自动停止
watch(ready, (val) => {
if (val) console.log('准备好了!')
}, { once: true })API 参考
watch()
| 项 | 内容 |
|---|---|
| 签名 | function watch<T>(source: WatchSource<T>, callback: WatchCallback<T>, options?: WatchOptions): WatchHandle |
| 多源签名 | function watch<T extends any[]>(sources: [...], callback: (...), options?: WatchOptions): WatchHandle |
| 入参 | source: 侦听源;callback: 回调函数;options: 配置项 |
| 返回值 | WatchHandle,可调用(等同 stop)+ 含 pause() / resume() / stop() 方法 |
| 版本 | ≥ 3.0.0;once ≥ 3.4.0;deep: number ≥ 3.5.0 |
WatchSource 类型
| 类型 | 示例 | 说明 |
|---|---|---|
Ref<T> | watch(count, cb) | 侦听 ref 的 .value |
() => T | watch(() => obj.x, cb) | getter 函数 |
T extends object | watch(state, cb) | reactive 对象(自动 deep) |
WatchSource[] | watch([r1, g2], cb) | 多源数组 |
WatchOptions
| 选项 | 类型 | 默认值 | 版本 | 说明 |
|---|---|---|---|---|
immediate | boolean | false | ≥ 3.0.0 | 是否立即执行回调(首次 oldValue 为 undefined) |
deep | boolean | number | false | ≥ 3.0.0 | true 深度侦听;3.5+ 支持数字指定最大遍历层数 |
flush | 'pre' | 'post' | 'sync' | 'pre' | ≥ 3.0.0 | 回调刷新时机(见下表) |
onTrack | (e: DebuggerEvent) => void | — | ≥ 3.2.0 | 依赖被追踪时调用(仅开发模式) |
onTrigger | (e: DebuggerEvent) => void | — | ≥ 3.2.0 | 依赖变化触发回调时调用(仅开发模式) |
once | boolean | false | ≥ 3.4.0 | 仅触发一次后自动停止 |
flush 刷新时机对比
| 值 | 执行时机 | 能访问更新后 DOM? | 批处理? | 典型场景 |
|---|---|---|---|---|
'pre' | 父组件更新后、当前组件 DOM 更新前 | ❌(拿到旧 DOM) | ✅ | 大多数侦听(默认) |
'post' | 组件 DOM 更新之后 | ✅ | ✅ | 需要读取布局信息(高度、位置) |
'sync' | 响应式数据变化时立即同步执行 | ⚠️(不确定) | ❌ | 罕见场景,需精确时序控制 |
WatchHandle(返回值)
| 方法 | 说明 | 版本 |
|---|---|---|
() 调用 | 停止侦听(等同于 stop()) | ≥ 3.0.0 |
stop() | 停止侦听 | ≥ 3.0.0 |
pause() | 暂停侦听(回调不触发,依赖不追踪) | ≥ 3.2.0 |
resume() | 恢复侦听 | ≥ 3.2.0 |
const { stop, pause, resume } = watch(count, () => {})
pause() // 暂停
count.value++
resume() // 恢复(不会自动执行回调,下次变化才会触发)
stop() // 彻底停止watchEffect()
| 项 | 内容 |
|---|---|
| 签名 | function watchEffect(effect: (onCleanup: OnCleanup) => void, options?: WatchEffectOptions): WatchHandle |
| 入参 | effect: 副作用函数;options: 配置项 |
| 返回值 | WatchHandle |
| 版本 | ≥ 3.0.0 |
WatchEffectOptions(watchEffect 专属)
| 选项 | 类型 | 默认值 | 说明 |
|---|---|---|---|
flush | 'pre' | 'post' | 'sync' | 'pre' | 同 watch |
onTrack | (e: DebuggerEvent) => void | — | 仅开发模式 |
onTrigger | (e: DebuggerEvent) => void | — | 仅开发模式 |
watchEffect没有immediate(本身就是立即执行的)、deep(自动追踪的)、once(需用watch+once替代)。
watchPostEffect() / watchSyncEffect()
| API | 等价于 | 版本 |
|---|---|---|
watchPostEffect(effect) | watchEffect(effect, { flush: 'post' }) | ≥ 3.2.0 |
watchSyncEffect(effect) | watchEffect(effect, { flush: 'sync' }) | ≥ 3.2.0 |
onWatcherCleanup()
| 项 | 内容 |
|---|---|
| 签名 | function onWatcherCleanup(cleanupFn: () => void): void |
| 来源 | import { onWatcherCleanup } from 'vue' |
| 调用位置 | watch 回调或 watchEffect 副作用函数内部 |
| 限制 | 必须在同步代码中调用,不能在 await 之后 |
| 版本 | ≥ 3.5.0 |
onCleanup 参数 vs onWatcherCleanup()
| 方式 | 用法 | 作用域 | 限制 |
|---|---|---|---|
onCleanup 参数 | watchEffect((onCleanup) => { onCleanup(fn) }) | 绑定到当前侦听器实例 | 无同步限制 |
onWatcherCleanup API | import { onWatcherCleanup } from 'vue' 然后直接调用 | 基于当前活跃的侦听器 | 不能在 await 后调用 |
// 旧方式:onCleanup 参数
watch(id, async (newId, oldId, onCleanup) => {
const controller = new AbortController()
onCleanup(() => controller.abort()) // ✅ 在 await 之前注册
const data = await fetch(`/api/${newId}`, { signal: controller.signal })
})
// 新方式:onWatcherCleanup(3.5+)
import { onWatcherCleanup } from 'vue'
watch(id, async (newId) => {
const controller = new AbortController()
onWatcherCleanup(() => controller.abort()) // ✅ 在 await 之前调用
const data = await fetch(`/api/${newId}`, { signal: controller.signal })
// onWatcherCleanup(() => ...) // ❌ 在这里调用会失败!(await 之后)
})类型定义
// ==================== 核心类型 ====================
// 侦听源
type WatchSource<T = any> = Ref<T> | (() => T) | (T extends object ? T : never)
// 回调函数
type WatchCallback<T> = (
value: T,
oldValue: T,
onCleanup: (cleanupFn: () => void) => void
) => void
// ==================== watch ====================
// 单源
function watch<T>(
source: WatchSource<T>,
callback: WatchCallback<T>,
options?: WatchOptions
): WatchHandle
// 多源
function watch<T extends Readonly<Array<WatchSource<unknown>>>>(
sources: T,
callback: (
values: MapSources<T, true>,
oldValues: MapSources<T, true>,
onCleanup: (cleanupFn: () => void) => void
) => void,
options?: WatchOptions
): WatchHandle
// ==================== watchEffect ====================
type WatchEffect = (onCleanup: OnCleanup) => void
function watchEffect(
effect: WatchEffect,
options?: WatchEffectOptions
): WatchHandle
// ==================== 别名 ====================
function watchPostEffect(effect: WatchEffect): WatchHandle
function watchSyncEffect(effect: WatchEffect): WatchHandle
// ==================== 选项类型 ====================
interface WatchOptions extends WatchEffectOptions {
immediate?: boolean // 默认 false
deep?: boolean | number // 默认 false;3.5+ 支持 number
once?: boolean // 默认 false(3.4+)
}
interface WatchEffectOptions {
flush?: 'pre' | 'post' | 'sync' // 默认 'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
// ==================== 返回值 ====================
interface WatchHandle {
(): void // 可调用,等同 stop()
pause: () => void
resume: () => void
stop: () => void
}
// ==================== 副作用清理 ====================
type OnCleanup = (cleanupFn: () => void) => void
function onWatcherCleanup(cleanupFn: () => void): void // 3.5+
// ==================== 调试事件 ====================
interface DebuggerEvent {
effect: ReactiveEffect
target: object
type: TrackOpTypes | TriggerOpTypes
key: any
}watch vs watchEffect 决策指南
| 需求 | watch | watchEffect | 说明 |
|---|---|---|---|
| 侦听特定数据源 | ✅ | ❌ | watchEffect 无法精确控制追踪范围 |
| 立即执行一次 | ⚠️ immediate | ✅(默认) | |
| 懒执行(变化才触发) | ✅(默认) | ❌ | |
| 获取旧值 | ✅ | ❌ | watchEffect 拿不到 oldValue |
| 多源侦听 | ✅ 数组 | ⚠️(全部自动追踪) | watchEffect 无法区分是哪个依赖变了 |
| 异步副作用 | ✅ | ✅ | 二者都在回调/副作用函数内部执行 |
| 纯自动追踪 | ⚠️ getter | ✅ | |
| 一次性侦听 | ✅ { once: true } | ❌ | |
| DOM 更新后执行 | ✅ { flush: 'post' } | ✅ { flush: 'post' } | |
| 需要深度侦听控制 | ✅ { deep: number } | ❌(自动全部) |
经验法则:需要精确控制(特定数据源、旧值对比、一次性)→
watch;只需**"状态变了就做某事"**且不关心具体哪个 →watchEffect。
注意事项与边界情况
侦听 reactive 对象的引用问题
const state = reactive({ count: 0 })
// ✅ 自动 deep:任意属性变化都触发
watch(state, (newVal, oldVal) => {
console.log(newVal === oldVal) // true —— deep 模式下是同一个引用!
})
// ✅ 用 getter 精确控制
watch(
() => state.count,
(newVal, oldVal) => {
console.log(newVal, oldVal) // 不同值,正常对比
}
)onWatcherCleanup 的同步限制
watch(id, async (newId) => {
// ✅ 在 await 之前注册
onWatcherCleanup(() => cleanup())
const data = await fetch(`/api/${newId}`)
// ❌ 这里调用会失败!组件可能已卸载或侦听器已重建
// onWatcherCleanup(() => otherCleanup())
})flush: 'sync' 的性能风险
const arr = ref([1, 2, 3])
// ❌ sync 模式无批处理,每个 push 单独触发 watch
watch(arr, () => {
console.log('触发')
}, { flush: 'sync' })
arr.value.push(4) // 触发 1 次(仅长度变化)
arr.value.push(5) // 触发 1 次
// ✅ 默认 'pre' 模式会批量合并组件卸载自动停止
<script setup lang="ts">
// 在 setup / <script setup> 中创建的侦听器
// 组件卸载时自动停止,无需手动调用 stop()
watch(count, () => {})
</script>但如果是在 setTimeout 或异步回调中创建侦听器,不会自动绑定到组件生命周期,需要手动停止。
侦听器不要嵌套修改自身依赖
// ❌ 可能导致无限循环
watch(count, (val) => {
count.value++ // 又触发 watch → 无限循环
})
// ✅ 加条件判断
watch(count, (val) => {
if (val < 10) count.value++
})常见误区
❌
watchEffect可以替代watch→ 不能。watchEffect无法获取旧值、无法精确控制追踪范围、无法一次性执行。二者互补。❌ 侦听
ref包裹的对象会自动 deep → 不会。watch(refObj, cb)只检测.value引用变化。要深度侦听需显式设置{ deep: true }。❌
deep: true时newVal !== oldVal→ 实际上是===。深度变化时新旧值是同一个引用,无法通过值对比判断哪些属性变了。❌
onWatcherCleanup可以在await之后调用 → 不行。它会报错:"onWatcherCleanup() must be called synchronously"。❌
flush: 'sync'只是更快而已 → 不是。"更快"意味着"没有批处理",多次连续修改会触发多次回调,性能开销远高于默认的'pre'。❌ 侦听器回调中可以安全操作 DOM → 默认
flush: 'pre'在 DOM 更新前执行,拿到的 DOM 是更新前的。需要操作 DOM 时用flush: 'post'或watchPostEffect。❌
watchEffect只追踪首次执行读取的依赖 → 每次重新执行都会重新收集依赖,动态依赖也能正确追踪。❌
watchEffect不能在回调中修改依赖 → 可以,但要小心无限循环。watchEffect内部修改的依赖如果在下次执行时又变化,会导致递归。
版本信息
| 版本 | 变更 |
|---|---|
| 3.0.0 | watch() / watchEffect() 首次发布(Proxy 响应式基础) |
| 3.2.0 | watchPostEffect() / watchSyncEffect() 别名;onTrack / onTrigger 调试钩子;pause() / resume() |
| 3.4.0 | { once: true } 一次性侦听 |
| 3.5.0 | deep: number 限制遍历深度;onWatcherCleanup() 全局清理 API |
来源
| 来源 | 链接 |
|---|---|
| 官方指南 — Watchers | vuejs.org/guide/essentials/watchers |
| 官方 API — Reactivity Core | vuejs.org/api/reactivity-core |
| 官方 API — onWatcherCleanup | vuejs.org/api/reactivity-core#onWatcherCleanup |
| 中文文档 — 侦听器 | cn.vuejs.org/guide/essentials/watchers |
| Vue 3.5 onWatcherCleanup 详解 | alexop.dev |
| 源码 — apiWatch.ts | github.com/vuejs/core |
最后整理:2026-07-08 · 对应 Vue 版本:3.5.x