一分钟先看结论(新手速读)

  • watch(谁, 回调, 配置):监听“谁”的变化,一变就执行回调。
  • 监听 ref 的基本类型(如数字、字符串)最简单,oldValue/newValue 很直观。
  • 监听 ref 包裹的对象时,想追踪内部属性变化通常要加 deep: true。
  • 监听 reactive 对象时,默认就是深度监听(能感知内部属性变化)。
  • 监听“某个属性”时,推荐写成函数:watch(() => obj.xxx, cb)。
  • 需要同时监听多个值时,用数组:watch([a, b, () => c.d], cb)。

1. 基础语法:watch 长什么样

watch(
  source, // 被监听的数据源
  (newValue, oldValue) => {
    // 变化后执行
  },
  options, // 可选:deep、immediate 等
)

常见 source 写法:

  • ref:watch(count, cb)
  • getter 函数:watch(() => state.name, cb)
  • 多数据源数组:watch([count, () => state.name], cb)

2. 用法 1:监听 ref 的基本类型

import { ref, watch } from 'vue'

const sum = ref(0)

const stopWatch = watch(sum, (newValue, oldValue) => {
  console.log('sum 变了', oldValue, '->', newValue)
  if (newValue >= 10) {
    stopWatch() // 手动停止监听
  }
})

要点:

  • 基本类型变化时,oldValue/newValue 很好理解,就是前后两个值。
  • watch 的返回值是“停止函数”,在不需要监听时可以手动调用。

3. 用法 2:监听 ref 的对象类型

import { ref, watch } from 'vue'

const person = ref({ name: 'Alice', age: 30 })

watch(
  person,
  (newValue, oldValue) => {
    console.log('person 变了', oldValue, '->', newValue)
  },
  { deep: true, immediate: true },
)

要点:

  • person.value.age++ 这种“改内部属性”属于对象内部变化,通常要 deep: true 才能稳定监听。
  • immediate: true 会在注册后立刻执行一次回调;第一次通常 oldValue 是 undefined。

4. 用法 3:监听 reactive 对象

import { reactive, watch } from 'vue'

const address = reactive({
  city: 'New York',
  zipcode: '10001',
})

watch(address, (newValue, oldValue) => {
  console.log('address 变了', oldValue, '->', newValue)
})

要点:

  • 直接监听 reactive 对象时,默认是深度监听。
  • 所以改 address.city、address.zipcode 都会触发回调。

5. 用法 4:监听对象里的某个属性(重点)

import { reactive, watch } from 'vue'

const company = reactive({
  name: 'Google',
  location: 'Mountain View',
  car: { c1: 'BMW', c2: 'Audi' },
})

watch(
  () => company.name,
  (newValue, oldValue) => {
    console.log('company.name 变了', oldValue, '->', newValue)
  },
)

watch(
  () => company.car,
  (newValue, oldValue) => {
    console.log('company.car 变了', oldValue, '->', newValue)
  },
  { deep: true },
)

要点(记这条最实用):

  • 监听“非对象属性”(如 name)时,必须写 getter:() => company.name。
  • 监听“对象属性”(如 car)建议也统一写 getter,心智负担更低,团队代码更一致。
  • 监听对象属性内部字段(如 car.c1)时,再配合 deep: true。

6. 用法 5:同时监听多个数据源

watch(
  [sum, () => person.value.age, () => company.name],
  (newValues, oldValues) => {
    console.log('多个数据变了', oldValues, '->', newValues)
  },
)

要点:

  • 把多个 source 放进数组即可。
  • 回调参数会变成数组,顺序与 source 数组一致。

7. 最容易踩坑:为什么 oldValue 和 newValue 看起来一样

当监听对象(尤其是深度监听)时,如果只是改了对象内部属性,oldValue 和 newValue 可能是同一个引用。

例如:

  • person.value.age += 1:对象没换,只是内部改了,可能看到“新旧值长得一样”。
  • person.value = { ... }:整个对象被替换,这时新旧引用才明显不同。

这不是 watch 失效,而是对象引用特性导致的正常现象。

8. 实战建议(给初学者)

  • 优先问自己:我监听的是“整个值”,还是“某个属性”?
  • 监听属性时优先用 getter 写法:() => xxx。
  • 对对象内部变化有需求时,再加 deep: true,不要无脑全加。
  • 初次加载就要执行逻辑时,用 immediate: true。
  • 在条件满足后及时调用停止函数,避免无意义监听。

9. 记忆口诀

基本类型看前后;对象内部要深搜;
监听属性用函数;多个来源放数组;
立刻执行加 immediate;不想再听就 stop。