Skip to content

watch / watchEffect 侦听器

watchwatchEffect 用于在响应式状态变化时执行副作用——API 请求、DOM 操作、持久化等。watch 显式指定侦听源、懒执行、可获取旧值;watchEffect 自动追踪依赖、立即执行、无需手动声明源。

速查卡片

特性watchwatchEffect
依赖追踪显式指定源自动收集
首次执行否(懒)是(立即)
获取旧值(newVal, oldVal)
多个源✅ 传入数组自动追踪所有读取的依赖
深度侦听{ deep: true }自动(副作用内访问即追踪)
副作用清理onCleanup 参数 / onWatcherCleanup(3.5+)onCleanup 参数 / onWatcherCleanup(3.5+)
暂停/恢复pause() / resume()pause() / resume()
一次性{ once: true }(3.4+)❌(可用 watch 替代)
调试钩子onTrack / onTriggeronTrack / onTrigger
版本≥ 3.0.0≥ 3.0.0
场景推荐
根据 ID 变化请求数据watch(id, async (newId) => ...)
自动收集依赖并执行副作用watchEffect
需要旧值对比watch
操作更新后的 DOMwatch + { flush: 'post' }watchPostEffect
简单派生新值computed(不用侦听器)

概述

computed 用于产生一个值,侦听器用于做一件事。当状态变化时需要执行 API 请求、操作 DOM、写 localStorage 等副作用时,用侦听器。

关于 computed 的完整用法及与侦听器的区别,详见 computed 计算属性

两个核心 API:

  • watch:显式指定侦听源,回调在源变化时触发。懒执行(默认不立即触发),可获取新旧值。
  • watchEffect:自动追踪副作用函数内访问的所有响应式依赖。立即执行一次以建立追踪,之后依赖变化自动重新执行。

watch() 详解

侦听源类型

watch() 的第一个参数可以是:

源类型示例说明
refwatch(count, ...)侦听 ref 的 .value 变化
getter 函数watch(() => state.x, ...)返回要侦听的值,精确控制
响应式对象(reactive)watch(state, ...)自动开启隐式 deep
以上类型的数组watch([ref1, getter2], ...)同时侦听多个源
typescript
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}`)
  }
)

回调参数

typescript
type WatchCallback<T> = (
  value: T,           // 新值
  oldValue: T,        // 旧值(首次为 undefined)
  onCleanup: (fn: () => void) => void  // 注册清理函数
) => void

侦听 reactive 对象的隐式 deep

当你直接传一个 reactive 对象作为源时,Vue 自动启用深度侦听——对象任意嵌套属性的变化都会触发回调:

typescript
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() 详解

自动追踪 + 立即执行

typescript
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 函数,用于注册清理逻辑。每当副作用重新执行前,上一次的清理函数会被调用:

typescript
watchEffect((onCleanup) => {
  const source = new EventSource('/api/events')
  
  onCleanup(() => {
    source.close() // 下次执行前自动关闭上一次的连接
  })
})

返回值:停止侦听

typescript
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' })响应式变化时同步触发
typescript
import { watchPostEffect, watchSyncEffect } from 'vue'

// 需要在回调中访问更新后的 DOM
watchPostEffect(() => {
  console.log(el.value?.offsetHeight) // 拿到最新高度
})

// 需要同步响应(慎用,无批处理)
watchSyncEffect(() => {
  // 每次依赖变化都同步执行,性能敏感场景慎用
})

基础用法

示例一:最简——watch 监听 ref 变化

vue
<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)

vue
<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 初始化加载

vue
<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

vue
<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+)

typescript
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+)

typescript
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
() => Twatch(() => obj.x, cb)getter 函数
T extends objectwatch(state, cb)reactive 对象(自动 deep)
WatchSource[]watch([r1, g2], cb)多源数组

WatchOptions

选项类型默认值版本说明
immediatebooleanfalse≥ 3.0.0是否立即执行回调(首次 oldValue 为 undefined
deepboolean | numberfalse≥ 3.0.0true 深度侦听;3.5+ 支持数字指定最大遍历层数
flush'pre' | 'post' | 'sync''pre'≥ 3.0.0回调刷新时机(见下表)
onTrack(e: DebuggerEvent) => void≥ 3.2.0依赖被追踪时调用(仅开发模式)
onTrigger(e: DebuggerEvent) => void≥ 3.2.0依赖变化触发回调时调用(仅开发模式)
oncebooleanfalse≥ 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
typescript
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 APIimport { onWatcherCleanup } from 'vue' 然后直接调用基于当前活跃的侦听器不能在 await 后调用
typescript
// 旧方式: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 之后)
})

类型定义

typescript
// ==================== 核心类型 ====================

// 侦听源
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 决策指南

需求watchwatchEffect说明
侦听特定数据源watchEffect 无法精确控制追踪范围
立即执行一次⚠️ immediate✅(默认)
懒执行(变化才触发)✅(默认)
获取旧值watchEffect 拿不到 oldValue
多源侦听✅ 数组⚠️(全部自动追踪)watchEffect 无法区分是哪个依赖变了
异步副作用二者都在回调/副作用函数内部执行
纯自动追踪⚠️ getter
一次性侦听{ once: true }
DOM 更新后执行{ flush: 'post' }{ flush: 'post' }
需要深度侦听控制{ deep: number }❌(自动全部)

经验法则:需要精确控制(特定数据源、旧值对比、一次性)→ watch;只需**"状态变了就做某事"**且不关心具体哪个 → watchEffect

注意事项与边界情况

侦听 reactive 对象的引用问题

typescript
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 的同步限制

typescript
watch(id, async (newId) => {
  // ✅ 在 await 之前注册
  onWatcherCleanup(() => cleanup())
  
  const data = await fetch(`/api/${newId}`)
  
  // ❌ 这里调用会失败!组件可能已卸载或侦听器已重建
  // onWatcherCleanup(() => otherCleanup())
})

flush: 'sync' 的性能风险

typescript
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' 模式会批量合并

组件卸载自动停止

vue
<script setup lang="ts">
// 在 setup / <script setup> 中创建的侦听器
// 组件卸载时自动停止,无需手动调用 stop()
watch(count, () => {})
</script>

但如果是在 setTimeout 或异步回调中创建侦听器,不会自动绑定到组件生命周期,需要手动停止。

侦听器不要嵌套修改自身依赖

typescript
// ❌ 可能导致无限循环
watch(count, (val) => {
  count.value++ // 又触发 watch → 无限循环
})

// ✅ 加条件判断
watch(count, (val) => {
  if (val < 10) count.value++
})

常见误区

  1. watchEffect 可以替代 watch → 不能。watchEffect 无法获取旧值、无法精确控制追踪范围、无法一次性执行。二者互补。

  2. 侦听 ref 包裹的对象会自动 deep → 不会。watch(refObj, cb) 只检测 .value 引用变化。要深度侦听需显式设置 { deep: true }

  3. deep: truenewVal !== oldVal → 实际上是 ===。深度变化时新旧值是同一个引用,无法通过值对比判断哪些属性变了。

  4. onWatcherCleanup 可以在 await 之后调用 → 不行。它会报错:"onWatcherCleanup() must be called synchronously"。

  5. flush: 'sync' 只是更快而已 → 不是。"更快"意味着"没有批处理",多次连续修改会触发多次回调,性能开销远高于默认的 'pre'

  6. 侦听器回调中可以安全操作 DOM → 默认 flush: 'pre' 在 DOM 更新前执行,拿到的 DOM 是更新前的。需要操作 DOM 时用 flush: 'post'watchPostEffect

  7. watchEffect 只追踪首次执行读取的依赖 → 每次重新执行都会重新收集依赖,动态依赖也能正确追踪。

  8. watchEffect 不能在回调中修改依赖 → 可以,但要小心无限循环。watchEffect 内部修改的依赖如果在下次执行时又变化,会导致递归。

版本信息

版本变更
3.0.0watch() / watchEffect() 首次发布(Proxy 响应式基础)
3.2.0watchPostEffect() / watchSyncEffect() 别名;onTrack / onTrigger 调试钩子;pause() / resume()
3.4.0{ once: true } 一次性侦听
3.5.0deep: number 限制遍历深度;onWatcherCleanup() 全局清理 API

来源

来源链接
官方指南 — Watchersvuejs.org/guide/essentials/watchers
官方 API — Reactivity Corevuejs.org/api/reactivity-core
官方 API — onWatcherCleanupvuejs.org/api/reactivity-core#onWatcherCleanup
中文文档 — 侦听器cn.vuejs.org/guide/essentials/watchers
Vue 3.5 onWatcherCleanup 详解alexop.dev
源码 — apiWatch.tsgithub.com/vuejs/core

最后整理:2026-07-08 · 对应 Vue 版本:3.5.x