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.propwatch(() => obj.prop, (newVal, oldVal) => { console.log('变化了:', newVal, oldVal)})
// 重新赋值 obj// obj.prop 等价于xf0067.propobj = reactive({ prop: 2 }) // 函数会访问新的 objfunction changeObjProp() { obj.prop++}// obj.prop = 3 // 此时会触发 watch,因为函数返回了新的 prop 值原理分析:
- “修改 xf0067.prop 会触发 Vue 全局响应式更新,进而重跑 watch effect” 这个说法不成立。Vue 不会因为某个响应式属性变化就重跑所有 effect。
- watch 触发规则是精确依赖,不是全局广播 watch 的触发条件只有一个:上一次 getter 执行时,被收集到的那些依赖里,有人触发了 set。
按你的代码流程:
- 创建 watch 时,getter 读取的是当时的 xf0066.prop,所以依赖绑定到 xf0066.prop。
- obj 重新赋值为 xf0067,只是普通变量赋值,不是响应式事件。
- 修改 xf0067.prop 时,只会触发依赖了 xf0067.prop 的 effect。
- 这个 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 })
在这个写法下会发生:
- immediate: true 时,先执行一次回调(初始化调用)。
- 之后要再次触发,前提是这个 getter 在上一次执行时收集到了响应式依赖,并且这些依赖发生变化。
- 但 () => person 只返回变量本身,没有读取 person.name、person.age 这类属性,所以基本没有可触发的依赖。
所以结论是:
- 不开 deep 时,这段代码实际只会跑初始化那一次(因为开了 immediate)。
- 后续 person.age 改变、person.car.brand 改变,通常都不会触发这个 watch。
- person = 新对象 也不会触发,因为这是普通变量重绑定,不是响应式依赖变更。
如果想“不开 deep 也有触发”,就要监听具体属性,例如:
- watch(() => person.age, …)
- 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),而不是变量名,也不是地址值本身。
可以这样对比:
- watch(() => person)
- 这里只读取了 JS 变量 person(词法变量)。
- 变量读取不会走 Vue 的 Proxy get 拦截。
- 所以默认几乎不建立依赖(不开 deep 时)。
- watch(() => person.car)
- 这里发生了对响应式对象 person 的属性读取 car。
- 会触发 Proxy get,Vue 记录依赖:(person对象, car键)。
- 之后只要 person.car 被 set,就能触发 watch。
- “car 解析出来也是地址值”
- 对,最终值可能也是对象引用。
- 但依赖建立发生在“读取 person.car 这一刻”,不是在比较地址值那一刻。
- Vue 的依赖桶是按 target + key 建的,不是按“某个地址变量名”建的。
再补一刀关键点:
- person = 新对象 这是变量重绑定,不是响应式 set,不触发依赖。
- person.car = 新对象 这是响应式对象属性 set,会触发依赖。
一句话总结: 同样都可能得到“引用值”,但只有通过响应式对象属性访问产生的读取,才会被 Vue 追踪。
赞助支持
如果这篇文章对你有帮助,欢迎赞助支持!
部分内容可能已过时