1. 问题本质:传递的是"值"而不是"响应式依赖"
当你写:
javascript
watch(obj.count, (newVal) => { ... })
实际执行过程是:
- 先计算
obj.count------此时obj是一个reactive代理对象,访问.count会触发依赖收集,并返回当前的值(比如0)。 - 然后把返回值
0作为第一个参数传给watch。 watch接收到的是一个原始数字 ,它不知道这个数字来自哪个响应式对象,更不知道当obj.count变化时需要重新执行回调。
换句话说:你传给 watch 的是一个快照,而不是一个"数据源"的引用。Vue 无法追踪一个固定数值的变化。
2. Getter 函数的作用:保留"追踪路径"
当你写:
javascript
watch(() => obj.count, (newVal) => { ... })
- 你传递给
watch的是一个函数,而不是具体的值。 watch内部会执行这个函数,并记录执行过程中访问到的所有响应式属性(这里是obj.count)。- Vue 知道:只要
obj.count发生变化,就需要重新执行这个 getter,并把新、旧值传递给回调。
这样,watch 就建立了一条响应式依赖→回调的链接。
3. 底层原理简述
Vue 3 的响应式系统基于 Proxy。每个响应式属性都有一个依赖收集器(Dep)。
watch需要知道它到底依赖了哪些属性,才能在这些属性变化时触发回调。- 直接传值:无法进行依赖收集。
- 传 getter:
watch运行 getter,getter 内的属性访问就会触发track(依赖收集),从而建立起映射。
类似的规则也适用于 computed、watchEffect 等 API。
4. 什么时候可以直接传值?
只有当你要侦听的对象本身是响应式对象(ref 或 reactive) ,并且你想侦听整个对象的变化时,才可以直接传递:
javascript
const state = reactive({ count: 0 })
watch(state, (newState) => { ... }) // 有效,侦听整个 state 对象
但如果你只关心对象内部的一个属性,就必须用 getter。
对于 ref,直接传递 ref 变量也可以:
javascript
const count = ref(0)
watch(count, (newVal) => { ... }) // 有效,因为 count 本身就是一个响应式引用
但如果是 count.value,又变成传值了:
javascript
watch(count.value, ...) // 错误,传递的是数字 0
5. 实际案例对比
javascript
import { reactive, watch } from 'vue'
const obj = reactive({ count: 0 })
// ❌ 错误写法 -- 只会执行一次,后续 obj.count 变化不会触发
watch(obj.count, (val) => {
console.log('直接传值:', val)
})
// ✅ 正确写法 -- 能正确响应变化
watch(() => obj.count, (val) => {
console.log('getter 方式:', val)
})
// 修改值
obj.count++ // 第一个 watch 无输出,第二个输出 "getter 方式: 1"
总结
| 方式 | 类型 | 能侦测变化? | 原因 |
|---|---|---|---|
watch(obj.count, ...) |
具体值(数字/字符串等) | ❌ | 失去响应式依赖,只拿到快照 |
watch(() => obj.count, ...) |
getter 函数 | ✅ | 内部追踪 .count 的访问,建立依赖 |
watch(obj, ...) |
响应式对象 | ✅ | 直接侦听整个对象 |
watch(refVal, ...) |
ref 对象 | ✅ | ref 本身是响应式引用 |
所以记住一句口诀:要侦听响应式对象的属性,必须用函数返回它;直接传值,Vue 不认得。 这是 Vue 3 响应式 API 的一个设计选择,目的在于明确追踪边界,避免性能浪费和意外行为。