Vue多响应式值场景下watch函数的正确用法及原理探究
Vue中watch未监听所有用到的响应式变量的正确用法及原理
一、React与Vue的依赖处理差异
在React中使用useEffect时,若回调内用到的响应式变量未加入依赖数组,会触发ESLint的exhaustive-deps错误:
const [foo, setFoo] = useState("foo") const [bar, setBar] = useState("bar") useEffect(() => { if (foo === 'something' && bar !== 'anything') { console.log({ foo, bar }) } }, [foo]) // Error: react-hooks/exhaustive-deps
但在Vue中,以下写法不会报错:
const foo = ref('foo'); const bar = ref('bar'); watch(foo, (newFoo) => { if (bar.value !== 'bar') { console.log(newFoo) } })
我曾认为应该监听所有用到的响应式变量,写法如下:
const foo = ref('foo'); const bar = ref('bar'); watch([foo, bar], ([newFoo, newBar]) => { if (newBar !== 'bar') { console.log(newFoo) } })
请问该场景下watch的正确用法是什么?背后原理为何?
二、测试代码
为验证两种写法的差异,我编写了以下测试代码:
<script setup> import { ref, watch } from 'vue' const foo = ref('foo') const bar = ref('bar') const fooChecked = ref(false) const isBarNotBar = ref(false) const foo2 = ref('foo2') const bar2 = ref('bar2') const foo2Checked = ref(false) const isBar2NotBar = ref(false) // 仅监听foo的写法 watch(foo, (newFoo) => { fooChecked.value = true if (bar.value !== 'bar') { isBarNotBar.value = true } }) // 监听foo和bar的写法 watch([foo2, bar2], ([newFoo2, newBar2]) => { foo2Checked.value = true if (newBar2 !== 'bar') { isBar2NotBar.value = true } }) // 异步修改bar的方法 function changeBarAsync(){ setTimeout(() => { bar.value = 'newBar' }, 0) } function changeBar2Async(){ setTimeout(() => { bar2.value = 'newBar' }, 0) } </script> <template> <div> {{ foo }} | {{ bar }} <div :style="isBarNotBar ? { color: 'red' } : { color: 'green' }"> {{ isBarNotBar ? 'bar is not bar' : 'bar is bar' }}{{ fooChecked ? ' ✅' : '' }} </div> </div> <br> <button @click="() => foo = 'newFoo'"> change foo </button> <button @click="() => { foo = 'newFoo'; bar = 'newBar'}"> change foo and bar </button> <button @click="() => { foo = 'newFoo'; changeBarAsync(); }"> change foo and bar async </button> <br > <br > <div :style="{ width: '100%', height: '1px', backgroundColor: '#777' }"></div> <br > <div> {{ foo2 }} | {{ bar2 }} <div :style="isBar2NotBar ? { color: 'red' } : { color: 'green' }"> {{ isBar2NotBar ? 'bar is not bar' : 'bar is bar' }}{{ foo2Checked ? ' ✅' : '' }} </div> </div> <br> <button @click="() => foo2 = 'newFoo'"> change foo2 </button> <button @click="() => { foo2 = 'newFoo'; bar2 = 'newBar'}"> change foo2 and bar2 </button> <button @click="() => { foo2 = 'newFoo'; changeBar2Async(); }"> change foo2 and bar2 async </button> <br > </template>
三、正确用法及原理
核心结论
两种写法都合法,关键取决于你的业务触发逻辑需求:
1. 仅监听特定变量的写法(单源监听)
如果你的需求是:只有当foo发生变化时,才执行回调逻辑并检查bar的当前值,那第一种写法完全没问题。
- 原理:Vue的响应式系统基于Proxy/Object.defineProperty,回调执行时会直接读取
bar.value的最新值,不存在React中闭包捕获旧值的问题。 - 局限性:如果
bar单独发生变化,这个watch不会触发——这是符合预期的,因为你只监听了foo的变化。
2. 监听多个变量的写法(多源监听)
如果你的需求是:只要foo或bar任意一个发生变化,都要执行回调逻辑,那必须使用第二种写法,将所有相关变量加入监听数组。
- 原理:Vue的watch支持监听数组形式的数据源,当数组中任意一个值变化时,都会触发回调,回调参数会返回所有数据源的最新值。
- 适用场景:比如你的逻辑依赖
foo和bar的组合状态,任何一个变量变化都需要重新判断逻辑。
测试场景的差异说明
在测试代码的"change foo and bar async"场景中:
- 单源监听写法:
foo先变化触发watch,此时bar还未完成异步修改,所以isBarNotBar保持false;之后bar异步变化,但因为没被监听,watch不会再次触发,isBarNotBar始终为false。 - 多源监听写法:
foo变化时触发一次watch,之后bar异步变化时会再次触发watch,此时读取到bar2的最新值,isBar2NotBar会变为true。
内容的提问来源于stack exchange,提问作者Bo Hyeon Seo
相关产品推荐
相关产品推荐

