Code前端首页关于Code前端联系我们

Vue3中的watch和watchEffect有啥区别?开发时该怎么选?

terry 2小时前 阅读数 36 #Vue

很多刚上手Vue3的开发者写侦听逻辑的时候都踩过这两个API的坑:要么明明要监听多个数据,给watch列了一大串侦听源,写得冗余不堪;要么用了watchEffect之后,回调动不动就被触发,半天找不到是哪个依赖变了;还有的人不知道watch默认懒执行,第一次进页面死活不触发数据请求,折腾半天才想起要加immediate配置,其实这两个API的定位完全不同,搞懂核心差异之后,选起来根本不会纠结。

核心运行逻辑的差异

首先我们得先搞懂两者最根本的运行逻辑不一样,watch本质是“主动指定侦听源的懒执行侦听器”,你得手动告诉它你要监听哪个数据,它才会盯着这个数据,只有你指定的这个源发生变化的时候,才会触发回调,而且默认情况下,页面第一次加载的时候它是不会跑的,只有后续数据变了才会执行。 而watchEffect是“自动收集依赖的立即执行侦听器”,你不用告诉它要监听谁,只要你在它的回调函数里用到了响应式数据,它会自动把这些数据收集成依赖,只要任何一个依赖发生变化,回调就会重新执行,而且默认页面第一次加载的时候就会跑一遍,一边跑一边收集需要监听的依赖。 举个很直观的例子,假设你有个用户信息对象,里面有姓名、年龄两个属性: ```javascript const user = reactive({ name: '张三', age: 18 }) ``` 如果你用watch监听,只会盯着你指定的属性: ```javascript // 只监听name属性,age变了完全不会触发 watch(() => user.name, (newVal) => { console.log('姓名变了', newVal) }) ``` 如果你用watchEffect,回调里用到的所有属性都会被监听: ```javascript watchEffect(() => { console.log(user.name, user.age) }) ``` 这种情况下,不管是name改了还是age改了,回调都会触发,而且页面刚加载的时候就会打印一次当前的姓名和年龄。 这两者的逻辑差异也带来了最直观的优缺点:watch的侦听精度更高,不会被无关的依赖变化影响,但是需要手动指定依赖,多依赖场景写起来麻烦;watchEffect不用手动列依赖,代码更简洁,但是如果依赖多了,很难排查是哪个变化导致的回调触发,也容易被无关的依赖影响性能。

回调可获取参数的差异

这也是很多开发者选择用watch的核心原因:watch的回调函数可以拿到变化前的旧值和变化后的新值,而watchEffect拿不到旧值,每次触发只能拿到最新的依赖值。 watch的回调默认接收两个参数,第一个是变化后的新值,第二个是变化前的旧值,对于需要做新旧值对比的场景来说简直是刚需,比如你要做个编辑页的离开提示,用户修改了表单内容之后点离开要弹窗提醒,你就可以用watch监听表单数据,对比新值和旧值是不是一致,要是不一致就标记为未保存: ```javascript const formData = reactive({ '', content: '' }) // 保存初始的表单数据 const originalForm = JSON.parse(JSON.stringify(formData)) watch(() => formData, (newVal) => { const isChanged = JSON.stringify(newVal) !== JSON.stringify(originalForm) // 标记是否有未保存的修改 unsavedChange.value = isChanged }, { deep: true }) ``` 要是你要做更细的对比,比如提示用户“您把标题从XX改成了XX”,直接拿oldVal.title和newVal.title对比就行,非常方便。 但如果用watchEffect的话,你是拿不到旧值的,要是需要做对比,你得自己在外面定义个变量来缓存上一次的值,每次回调执行完更新缓存,额外多了不少冗余代码: ```javascript const prevTitle = ref(formData.title) watchEffect(() => { if (formData.title !== prevTitle.value) { console.log(`标题从${prevTitle.value}改成了${formData.title}`) // 更新缓存 prevTitle.value = formData.title } }) ``` watch还支持第三个参数onCleanup,用来清理副作用,而watchEffect的onCleanup是作为回调的第一个参数传入的,虽然功能差不多,但写法上有区别,这个后面讲副作用清理的时候会细说。

执行时机的配置差异

两者都支持通过flush参数配置回调的执行时机,默认值都是'pre',也就是在组件更新前、DOM还没重新渲染的时候执行回调,这种情况下你在回调里拿到的DOM还是更新前的,如果你需要操作更新后的DOM,可以把flush设为'post',回调就会在DOM更新完成之后执行。 但两者的默认执行时机还是有明显差异:watch默认是懒执行的,如果你不手动开immediate: true的配置,第一次页面加载的时候完全不会执行,只有后续侦听源变化才会触发;而watchEffect默认是立即执行的,页面第一次加载就会跑一遍,不用额外配置。 这个差异在很多业务场景里影响很大,比如你要做个搜索页,进来就要根据默认的关键词、分类拉取第一页的数据,后续搜索条件变了还要重新拉: ```javascript // 用watch的话,必须加immediate: true才会第一次就拉数据 watch([keyword, category, page], () => { fetchList() }, { immediate: true }) // 用watchEffect的话,默认就会第一次执行,不用加额外配置 watchEffect(() => { fetchList(keyword.value, category.value, page.value) }) ``` 这种情况下用watchEffect明显更简洁,少了一行配置,但如果你要做的是只有用户修改了某个值之后才触发的逻辑,比如修改用户名之后提示保存成功,用watch就更合适,不会刚进页面就弹提示。

副作用清理与停止侦听的差异

两者都支持返回一个停止函数,调用这个函数就可以手动停止侦听,这个功能在需要临时关闭侦听的场景很有用,比如编辑页进入只读模式之后,就不需要再监听表单变化提示未保存了: ```javascript // watch的停止函数 const stopWatch = watch(() => formData, () => { unsavedChange.value = true }, { deep: true }) // watchEffect的停止函数 const stopEffect = watchEffect(() => { if (formData.title) { unsavedChange.value = true } }) // 进入只读模式时停止侦听 const readOnly = ref(false) watch(readOnly, (newVal) => { if (newVal) { stopWatch() stopEffect() } }) ``` 而在副作用清理的写法上两者有细微差别:watch的onCleanup是回调的第三个参数,而watchEffect的onCleanup是回调的第一个参数,副作用清理最常用的场景就是解决请求竞态问题,比如搜索框输入的时候,用户输得很快,前一个搜索请求还没返回,用户又输入了新的关键词,这时候就需要取消前一个未完成的请求,避免旧的请求结果覆盖新的: ```javascript // watch的副作用清理写法 watch(keyword, (newVal, oldVal, onCleanup) => { const controller = new AbortController() fetch(`/api/search?keyword=${newVal}`, { signal: controller.signal }).then(res => res.json()).then(data => { console.log('搜索结果', data) }) // 下一次回调触发前会执行这个清理函数,取消前一个请求 onCleanup(() => { controller.abort() }) }, { immediate: true })

// watchEffect的副作用清理写法 watchEffect((onCleanup) => { const controller = new AbortController() fetch(/api/search?keyword=${keyword.value}, { signal: controller.signal }).then(res => res.json()).then(data => { console.log('搜索结果', data) }) onCleanup(() => { controller.abort() }) })


<h3>实际开发中到底该怎么选?</h3>
看完上面的差异,其实选哪个的标准已经很清晰了,完全可以根据你的需求来:
1. 如果你需要精准监听某一个或某几个特定的数据,不需要监听其他无关数据,或者需要用到新旧值做对比,首选watch,比如监听路由参数的id变化拉取详情页数据、监听表单某个字段的变化做格式校验、监听开关状态切换联动其他字段,这些场景用watch都更合适,可控性更强,不会被无关的依赖变化触发。
2. 如果你需要监听多个依赖,只要任何一个依赖变化都要执行同一段逻辑,而且不需要用到旧值,首选watchEffect,比如多条件筛选的列表页,只要关键词、日期范围、分类、页码任何一个变了都要重新拉列表;比如根据多个响应式变量计算导出文件的文件名,只要任何一个变量变了文件名就要更新,这些场景用watchEffect不用手动列一堆侦听源,代码简洁很多。
3. 如果你需要的逻辑第一次页面加载就要执行,而且不需要新旧值对比,优先选watchEffect,省得给watch加immediate配置。
4. 如果你需要监听的逻辑里有动态依赖,也就是依赖是根据条件变化的,比如只有某个开关打开的时候才需要监听某个字段,这种情况下优先选watch手动指定依赖,避免watchEffect收集依赖不全的坑,比如下面这个场景:
```javascript
const showSearch = ref(false)
// 用watchEffect的话,showSearch初始为false的时候,不会收集keyword的依赖,后面showSearch变成true之后,keyword变化不会触发回调
watchEffect(() => {
  if (showSearch.value) {
    console.log(keyword.value)
  }
})
// 用watch的话,不管showSearch是什么状态,只要keyword变了都会触发,不会有依赖收集不全的问题
watch(keyword, (newVal) => {
  if (showSearch.value) {
    console.log(newVal)
  }
})

使用时的常见踩坑点

最后说几个大家日常开发里经常踩的坑,避免走弯路: 1. 不要直接用watch监听整个reactive对象,默认情况下watch监听reactive对象会强制开启深度监听,而且拿不到正确的旧值,因为reactive对象的新旧值是同一个引用,如果需要监听reactive对象的属性,最好用函数返回具体属性的写法,) => user.name,这样就能拿到正确的旧值,也不会默认开启深度监听,性能更好。 2. 不要在watchEffect里写太多和响应式无关的逻辑,也不要在里面访问太多不需要的响应式数据,不然会导致不必要的回调触发,既影响性能,出问题的时候也很难排查是哪个依赖变化导致的,如果依赖超过3个,而且逻辑比较复杂,建议还是用watch手动列依赖,更清晰可控。 3. 不要为了少写几行代码强行用watchEffect,比如明明只需要监听一个数据,还要用watchEffect,不仅多了自动收集依赖的开销,还拿不到旧值,反而得不偿失。

watch和watchEffect没有绝对的优劣,只是适用场景不同,watch更精准、可控性强,适合需要明确侦听目标、需要新旧值对比的场景;watchEffect更简洁、自动化,适合多依赖、不需要旧值的场景,不用纠结哪个更“高级”,能满足你的需求、写出来的代码好维护的就是最好的选择。

版权声明

本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。

热门