Vue 3 Watch 监视完全指南

Vue 3 Watch 监视完全指南

一、watch 基础概念

watch 用于侦听响应式数据源的变化 ,并在变化时执行副作用函数。它默认是惰性 的(不会立即执行),且能获取新值和旧值。

基本语法

js 复制代码
watch(source, callback, options)
  • source:侦听源(ref、reactive、getter 函数、数组)
  • callback :(newVal, oldVal, onCleanup) => {}
  • options:配置项(immediate、deep、flush、once 等)

二、侦听不同数据源

1. 侦听 ref

vue 复制代码
<script setup>
import { ref, watch } from 'vue'

const count = ref(0)

watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} -> ${newVal}`)
})

count.value++ // 输出: count: 0 -> 1
</script>

2. 侦听 reactive 对象

⚠️ 注意 :侦听 reactive 对象时,必须使用 getter 函数 ,否则会隐式创建深度侦听,且新值等于旧值。

js 复制代码
import { reactive, watch } from 'vue'

const state = reactive({ name: 'Tom', age: 18 })

// ❌ 错误:直接传 reactive,新旧值相同
watch(state, (newVal, oldVal) => {
  console.log(newVal === oldVal) // true(因为都是同一个代理对象)
})

// ✅ 正确:使用 getter
watch(
  () => state.name,
  (newVal, oldVal) => {
    console.log(`name: ${oldVal} -> ${newVal}`)
  }
)

// ✅ 侦听整个 reactive 需要 deep
watch(
  () => state,
  (newVal) => console.log(newVal),
  { deep: true }
)

3. 侦听 getter 函数

js 复制代码
const user = reactive({ first: '张', last: '三' })

// 侦听组合值
watch(
  () => user.first + user.last,
  (newVal, oldVal) => {
    console.log(`全名: ${oldVal} -> ${newVal}`)
  }
)

4. 侦听多个源(数组)

js 复制代码
const a = ref(1)
const b = ref(2)

watch([a, b], ([newA, newB], [oldA, oldB]) => {
  console.log(`a: ${oldA}->${newA}, b: ${oldB}->${newB}`)
})

// 混合写法
watch([a, () => b.value * 2], ([newA, newDoubleB]) => {
  console.log(newA, newDoubleB)
})

三、核心配置项 options

1. immediate ------ 立即执行

js 复制代码
const id = ref(1)

watch(
  id,
  (newVal) => {
    console.log('立即执行一次:', newVal) // 首次运行就输出 1
  },
  { immediate: true }
)

常用于组件初始化时根据 props 拉取数据。

2. deep ------ 深度侦听

js 复制代码
const obj = ref({ nested: { count: 0 } })

// ❌ 不加 deep,修改嵌套属性不会触发
watch(obj, () => console.log('触发'))
obj.value.nested.count++ // 不会触发

// ✅ 加 deep
watch(
  obj,
  () => console.log('触发'),
  { deep: true }
)
obj.value.nested.count++ // 触发

reactive 对象的特殊情况:直接侦听 reactive 对象时,Vue 会自动启用 deep:

js 复制代码
const state = reactive({ nested: { count: 0 } })

watch(state, () => {
  console.log('自动深度侦听')
})
state.nested.count++ // 触发

性能优化:使用 getter 精确侦听,避免 deep 全量遍历。

js 复制代码
// ✅ 只侦听特定属性,性能更好
watch(() => obj.value.nested.count, () => {})

3. flush ------ 回调执行时机

值 说明
'pre'(默认) 组件更新前执行
'post' 组件更新后执行
'sync' 同步执行,变化立即触发
js 复制代码
watch(source, cb, { flush: 'post' })  // 可访问更新后的 DOM
watch(source, cb, { flush: 'sync' })  // 谨慎使用,性能开销大

4. once ------ 只执行一次(Vue 3.4+)

js 复制代码
watch(
  source,
  (newVal) => {
    console.log('只执行一次') // 变化后只触发一次
  },
  { once: true }
)

5. onCleanup ------ 清理副作用

用于取消上一次的异步请求、定时器等,防止竞态问题。

js 复制代码
watch(id, async (newId, oldId, onCleanup) => {
  const controller = new AbortController()
  onCleanup(() => controller.abort()) // 下次触发前先取消上次请求

  const res = await fetch(`/api/user/${newId}`, {
    signal: controller.signal
  })
  const data = await res.json()
  console.log(data)
})

四、停止侦听

watch 返回一个停止函数,调用它可手动停止侦听。

js 复制代码
const stop = watch(count, () => {
  console.log('count 变化了')
})

count.value++ // 触发
stop()        // 停止侦听
count.value++ // 不再触发

注意 :在 setup() 或 <script setup> 中创建的 watch,会自动绑定到组件实例,组件卸载时自动停止。只有在异步回调中创建的 watch 才需要手动停止。

js 复制代码
// 异步中创建,需要手动清理
setTimeout(() => {
  const stop = watch(source, cb)
  onUnmounted(stop)
}, 1000)

五、watchEffect ------ 自动追踪

watchEffect 会立即执行一次 ,并自动收集回调中用到的响应式依赖。

js 复制代码
const count = ref(0)
const name = ref('Tom')

watchEffect(() => {
  // 自动追踪 count 和 name
  console.log(`count=${count.value}, name=${name.value}`)
})
// 立即输出一次,之后任一变化都触发

watch vs watchEffect 对比

特性 watch watchEffect
执行时机 默认惰性 立即执行
依赖收集 显式指定 自动收集
旧值 可获取 无法获取
适用场景 需新旧值对比、精确控制 简单副作用

watchEffect 的停止与清理

js 复制代码
const stop = watchEffect((onCleanup) => {
  const timer = setInterval(() => {}, 1000)
  onCleanup(() => clearInterval(timer))
})

stop() // 停止

watchPostEffect 和 watchSyncEffect

js 复制代码
import { watchPostEffect, watchSyncEffect } from 'vue'

// flush: 'post'
watchPostEffect(() => {
  // DOM 更新后执行
})

// flush: 'sync'
watchSyncEffect(() => {
  // 同步执行
})

六、实战案例

案例 1:搜索框防抖

vue 复制代码
<script setup>
import { ref, watch } from 'vue'

const keyword = ref('')
const results = ref([])

let timer = null

watch(keyword, (newVal) => {
  clearTimeout(timer)
  timer = setTimeout(async () => {
    if (!newVal) {
      results.value = []
      return
    }
    const res = await fetch(`/api/search?q=${newVal}`)
    results.value = await res.json()
  }, 300)
})
</script>

<template>
  <input v-model="keyword" placeholder="搜索..." />
  <ul>
    <li v-for="item in results" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

案例 2:侦听 props 变化

vue 复制代码
<script setup>
import { watch, toRefs } from 'vue'

const props = defineProps({
  userId: Number
})

// ✅ 推荐:getter 写法
watch(
  () => props.userId,
  async (newId) => {
    if (newId) {
      await loadUser(newId)
    }
  },
  { immediate: true }
)
</script>

案例 3:多源侦听 + 深度侦听

js 复制代码
const form = reactive({
  name: '',
  address: { city: '', street: '' }
})

watch(
  [() => form.name, () => form.address.city],
  ([newName, newCity], [oldName, oldCity]) => {
    console.log(`name: ${oldName}->${newName}`)
    console.log(`city: ${oldCity}->${newCity}`)
  },
  { immediate: true }
)

案例 4:避免竞态的异步请求

js 复制代码
watch(
  () => props.id,
  async (newId, oldId, onCleanup) => {
    let cancelled = false
    onCleanup(() => (cancelled = true))

    const data = await fetchData(newId)
    if (!cancelled) {
      // 确保只渲染最新请求的结果
      result.value = data
    }
  },
  { immediate: true }
)

七、常见坑点总结

  1. 侦听 reactive 对象直接传参会失效:必须用 getter,否则新旧值相同。
  2. ref 对象的 .value :侦听 ref 本身传 ref,侦听属性传 () => ref.value.xxx。
  3. deep 性能问题:大对象深度侦听开销大,优先用 getter 精确侦听。
  4. 数组/对象引用变化 :watch 侦听的是引用,直接修改属性需 deep: true。
  5. 异步创建的 watch 不会自动清理 :需要手动调用停止函数,或用 onUnmounted 清理。
  6. watchEffect 无法获取旧值 :需要新旧值对比时用 watch。
  7. flush: 'sync' 慎用:频繁触发会导致性能问题。

八、速查表

场景 写法
侦听单个 ref watch(ref, cb)
侦听 reactive 属性 watch(() => obj.key, cb)
侦听整个 reactive watch(obj, cb)(自动 deep)
侦听多个源 watch([a, b], cb)
立即执行 { immediate: true }
深度侦听 { deep: true }
DOM 更新后执行 { flush: 'post' }
只执行一次 { once: true }
自动追踪依赖 watchEffect(fn)
停止侦听 const stop = watch(...); stop()

掌握以上内容,即可应对 Vue 3 中绝大多数 watch 使用场景。核心原则:能精确侦听就不用 deep,能用 getter 就不传对象,异步副作用记得清理。

相关推荐
航飞光电市场经理2 小时前
人员定位系统“全栈自研”技术解析:从射频前端到定位引擎的架构拆解
前端·架构
Apifox2 小时前
Apifox 9 月更新|CLI 能力升级、GitLab 私有化部署接入与产品体验优化
前端·后端·测试
deli0072 小时前
随机乱跳为什么能画出完美三角形:混沌游戏分形实验室
前端
Gizzap_Tech2 小时前
国内官网增加英文版:URL、语言切换与 hreflang 怎么配置?
前端
剪刀石头布啊2 小时前
泛洪DFS、BFS
前端
qetfw2 小时前
Windows Server AD CS:根 CA、Web 证书模板与域内自动注册
前端·windows·windows-server
剪刀石头布啊2 小时前
为什么React组件很少前缀,而vue的不少组件都有前缀
前端
剪刀石头布啊2 小时前
gird网格布局
前端
瞬时出道2 小时前
AI 回答为什么能一个字一个字蹦出来?前端搞懂 SSE 这一篇就够了
vue.js