2185 字
11 分钟

watch函数

欢迎来到我的博客!#

使用 Markdown 格式编写。

今天学到了什么#

  • 这篇文章可以和昨天那篇可以结合起来看,相似度很高

首先先看函数式与直接写法的区别#

核心区别其实是“监听源是什么”,不是“函数更高级”。

直接写法:把响应式源本体传给 watch#

常见是 watch(person, cb)watch(countRef, cb)
特点:

  • 依赖目标固定:就是这个源本体。
  • watch(person, cb) 对 reactive 对象会做深层追踪(内部属性改动可触发)。(默认开启了深度监视,且不可关闭)
  • 不能写 watch(person.age, cb),因为这只是一个普通值,不是响应式源。

函数式写法:把 getter 传给 watch#

常见是 watch(() => person.age, cb)
特点:

  • 只追踪 getter 运行时访问到的依赖,更精确。
  • 可组合表达式,比如 watch(() => person.age + count.value, cb)
  • 适合监听“某个字段”或“计算结果”。

最容易误解的一点#

如果你是 let person = reactive(...),然后做 person = reactive(...),这是普通变量重绑定,不是响应式更新。
所以无论直接写法还是函数式写法,都不会仅因这次重绑定自动触发。

什么时候用哪种#

  • 想监听整个对象内部变化:用 watch(person, cb)
  • 想监听单个属性/表达式:用 watch(() => person.age, cb)
  • 想让“整体替换对象”也可被监听:用 ref 包对象,例如 const person = ref({...}),再 watch(() => person.value.age, cb)watch(person, cb)

一句话总结:
直接写法监听“这个响应式源本体”,函数式监听“getter 返回值及其访问路径”。

错误案例分析#

let obj = reactive({ prop: 1 })
// 函数式写法
//obj.prop 等价于xf0066.prop
watch(() => obj.prop, (newVal, oldVal) => {
console.log('变化了:', newVal, oldVal)
})
// 重新赋值 obj
// obj.prop 等价于xf0067.prop
obj = reactive({ prop: 2 }) // 函数会访问新的 obj
function changeObjProp() {
obj.prop++
}
// obj.prop = 3 // 此时会触发 watch,因为函数返回了新的 prop 值

原理分析:#

  1. “修改 xf0067.prop 会触发 Vue 全局响应式更新,进而重跑 watch effect” 这个说法不成立。Vue 不会因为某个响应式属性变化就重跑所有 effect。
  2. watch 触发规则是精确依赖,不是全局广播 watch 的触发条件只有一个:上一次 getter 执行时,被收集到的那些依赖里,有人触发了 set。

按你的代码流程:#

  1. 创建 watch 时,getter 读取的是当时的 xf0066.prop,所以依赖绑定到 xf0066.prop。
  2. obj 重新赋值为 xf0067,只是普通变量赋值,不是响应式事件。
  3. 修改 xf0067.prop 时,只会触发依赖了 xf0067.prop 的 effect。
  4. 这个 watch 之前没依赖 xf0067.prop,所以不会被触发。

总结:#

  • watch 监听的是“getter 上次执行时访问到的响应式属性”。
  • 只有这些已收集依赖触发 set,watch 才会重新执行 getter。
  • 不存在“某个属性变化导致所有 effect 全局重跑”。

验证函数式与直接写法的区别#

let person = reactive({
name: '张三',
age: 18,
car: {
brand: '奔驰',
color: '红色'
}
})
watch(() => person ,(newVal,oldVal)=>{
console.log(newVal,oldVal)
},
{
deep: true,
immediate: true
}
)
//点击changePerson按钮(改变person地址值),在点击changeOldPerson(触发watch函数,监听到person新的地址值)
/*一开始,watch监听的是person对象,当触发changePerson按钮时,person会被重新赋值,但是这并不会触发watch函数,
(因为person只是一个变量,不论是ref还是reactive,对变量的改变都不会触发watch函数的执行,因为没法检测变量的变换,否则会消耗巨大的性能),
也就是说,现在watch监听的还是oldPerson所指向的那个对象,当点击changeOldPerson按钮时,watch函数会被触发用,但由于是函数写法,函数会访问到新的 person 实例,此时的newValue就是新的 person 实例*/
let oldPerson = person
function changeOldPerson() {
oldPerson.name = '李四'
}
function changePerson(){
person = {
name: 'Mike',
age: 15,
car: {
brand: 'Audi',
color: 'black'
}
}
}

这和我昨天写的那个模板更新案例很像,直接改变量不会触发模板更新,但是旧的依赖发生变化后,模板可以获取到变量改变后的值

关键区别是“监听源是否固定”。

这个注释场景里,函数式能出现“先改 person 引用,再通过 oldPerson 的变化把新 person 打出来”,本质上是这条链路:

初始化时#

  • 直接写法 watch(person, cb):source 已经固定为最初那个响应式对象(oldPerson)。
  • 函数式写法 watch(() => person, cb, { deep: true }):source 是一个 getter,每次触发时都会重新读取当前变量 person。

执行 person = 新对象#

  • 这一步本身不是响应式触发(只是变量重绑定),两种写法都不会立刻触发回调。

再改 oldPerson 的某个属性#

  • 会触发之前建立在 oldPerson 上的依赖,watch 开始重新执行。
  • 直接写法:重新执行时 source 仍是“当初传入的那个对象”(固定的 oldPerson),所以拿不到新 person。
  • 函数式写法:重新执行时会再次运行 getter,getter 此时读到的是“当前 person 变量”,所以可能读到新 person。

所以你看到的“函数式可以、直接写法不可以”,不是因为函数式能监听变量重赋值,而是因为函数式在被触发后会重新取值,直接写法不会切换 source。

再强调一个容易误解的点:

  • 函数式也不能直接监听 person = 新对象 这一步。
  • 它只是借助“旧依赖触发的一次重跑”,顺带读到了新 person。

这里要注意,#

watch(() => person ,(newVal,oldVal)=>{
console.log(newVal,oldVal)
},
{
deep: true,
immediate: true
}
)

不开深度监视时,这段是“监听 getter 的返回值(引用)”,但几乎等于没监听到有效依赖。

现在是:

watch(() => person, cb, { immediate: true })

在这个写法下会发生:

  1. immediate: true 时,先执行一次回调(初始化调用)。
  2. 之后要再次触发,前提是这个 getter 在上一次执行时收集到了响应式依赖,并且这些依赖发生变化。
  3. 但 () => person 只返回变量本身,没有读取 person.name、person.age 这类属性,所以基本没有可触发的依赖。

所以结论是:

  1. 不开 deep 时,这段代码实际只会跑初始化那一次(因为开了 immediate)。
  2. 后续 person.age 改变、person.car.brand 改变,通常都不会触发这个 watch。
  3. person = 新对象 也不会触发,因为这是普通变量重绑定,不是响应式依赖变更。

如果想“不开 deep 也有触发”,就要监听具体属性,例如:

  1. watch(() => person.age, …)
  2. watch(() => person.car.brand, …)
watch(() => person.car ,(newVal,oldVal)=>{
console.log(newVal,oldVal)
},
{
deep: true,
immediate: true
}
)
function changeCar(){
//最好用reactive定义,这样还是响应式的
person.car = {
brand: 'Audi',
color: 'black'
}
}

对比这段代码,car是响应式属性,可以被追踪,而上一段代码的返回值只是一个普通的字面量

car是响应式属性,可以被追踪,而上一段代码的返回值只是一个普通的字面量,但是实际解析的时候,car也不过是一个变量,解析出来也是一个地址值,两者有什么区别吗?

这个问题问到点子上了。核心区别不在“是不是地址值”,而在“有没有触发 Proxy 的属性读取”。

Vue 追踪依赖看的是这件事: 读取了哪个响应式对象的哪个属性键(target + key),而不是变量名,也不是地址值本身。

可以这样对比:

  1. watch(() => person)
  • 这里只读取了 JS 变量 person(词法变量)。
  • 变量读取不会走 Vue 的 Proxy get 拦截。
  • 所以默认几乎不建立依赖(不开 deep 时)。
  1. watch(() => person.car)
  • 这里发生了对响应式对象 person 的属性读取 car。
  • 会触发 Proxy get,Vue 记录依赖:(person对象, car键)。
  • 之后只要 person.car 被 set,就能触发 watch。
  1. “car 解析出来也是地址值”
  • 对,最终值可能也是对象引用。
  • 但依赖建立发生在“读取 person.car 这一刻”,不是在比较地址值那一刻。
  • Vue 的依赖桶是按 target + key 建的,不是按“某个地址变量名”建的。

再补一刀关键点:

  • person = 新对象 这是变量重绑定,不是响应式 set,不触发依赖。
  • person.car = 新对象 这是响应式对象属性 set,会触发依赖。

一句话总结: 同样都可能得到“引用值”,但只有通过响应式对象属性访问产生的读取,才会被 Vue 追踪。

赞助支持

如果这篇文章对你有帮助,欢迎赞助支持!

赞助
最后更新于 2026-04-27,距今已过 127 天

部分内容可能已过时

目录