Vue3里的watch和watchEffect到底有什么区别?该怎么选?
很多入门Vue3的前端开发者,刚开始用组合式API做项目的时候,总会卡在watch和watchEffect的选择上:两个都是用来侦听响应式数据变化触发回调的API,看起来功能差不多,到底什么时候用哪个?这篇就把两个API的差异、踩坑点和选择逻辑讲得明明白白,看完再也不会用错。
先搞懂两个API的基础用法,从根上找差异
要搞清楚两者的区别,得先从各自的基础逻辑和用法说起,两个API的设计初衷本身就有差异。 先说watch,它是Vue2版本watch API的延续,核心逻辑是“显式指定侦听对象,仅当侦听对象变化时触发回调”,使用的时候你必须明确告诉它要侦听哪个响应式数据,默认是惰性执行的,也就是页面首次加载的时候不会运行回调,只有当你指定的侦听对象发生变化时才会触发,同时回调函数能拿到变化后的新值和变化前的旧值,比如你要做搜索功能,用户输入关键词后才触发接口请求,用watch写的话逻辑非常清晰: ```js import { ref, watch } from 'vue' const searchKey = ref('') // 显式指定侦听searchKey watch(searchKey, (newVal, oldVal) => { // 只有searchKey变化时才会触发请求,首次加载不会执行 fetch(`/api/goods?keyword=${newVal}`) }) ``` 如果需要首次加载就执行一次,只需要加个immediate: true的配置项就行,要是要侦听嵌套对象的深层属性,加个deep: true配置就可以。而watchEffect是Vue3新增的API,核心逻辑是“自动收集依赖,只要回调里用到的响应式数据变化,就重新执行回调”,你不需要提前指定要侦听谁,它会在第一次执行的时候自动收集所有用到的响应式依赖,后续任何一个依赖发生变化,都会重新触发回调,而且它默认不是惰性的,页面首次加载的时候就会执行一次,用来完成依赖收集,比如你要做商品列表筛选,筛选条件有关键词、分类、排序方式三个,只要任意一个条件变了就要重新拉列表,用watchEffect写就非常省事:
import { ref, watchEffect } from 'vue'
const searchKey = ref('')
const categoryId = ref(0)
const sortType = ref('default')
// 不需要指定侦听对象,自动收集三个变量的依赖
watchEffect(() => {
// 首次加载自动执行一次,后续三个变量任意一个变化就重新执行
fetch(`/api/goods?keyword=${searchKey.value}&cid=${categoryId.value}&sort=${sortType.value}`)
})
不用把三个依赖都列出来,也不用手动开immediate,代码比写watch要简洁很多。
核心差异对比,看完再也不会搞混
两个API的核心差异主要集中在四个方面,对应不同的业务场景需求: 第一个差异是依赖声明方式不同,watch是显式声明依赖,你要侦听谁必须提前写清楚,回调里用到的其他响应式数据变化不会触发回调;而watchEffect是自动收集依赖,只要回调里用到的响应式数据,不管有多少个,都会被纳入侦听范围,这就导致watch的依赖非常可控,不会出现意外触发的情况,而watchEffect更适合多依赖的场景,能省掉很多冗余的依赖声明代码。 第二个差异是默认执行时机不同,watch默认是惰性执行,首次加载不会触发回调,只有侦听的依赖变化才会触发;watchEffect默认首次加载就执行,用来完成依赖收集,当然两者都可以通过配置修改执行时机,watch加immediate: true就能首次执行,watchEffect也可以通过配置flush参数调整是在DOM更新前还是更新后运行。 第三个差异是旧值获取能力不同,watch的回调函数默认会返回新值和旧值两个参数,你可以直接对比变化前后的数值;而watchEffect拿不到旧值,只能拿到最新的响应式数据状态,如果你需要做变化前后的对比,比如记录用户修改前的表单内容,或者做数值变化的差值计算,就只能用watch。 第四个差异是侦听粒度的控制灵活度不同,watch可以精确控制侦听的粒度,比如你要侦听一个复杂对象,既可以侦听整个对象的变化,也可以只侦听对象下的某一个属性,还可以通过deep配置控制是否要侦听深层嵌套的属性;而watchEffect的粒度完全由你回调里用到的依赖决定,用到多少就侦听多少,没法单独控制某一个依赖的侦听深度。常见踩坑场景避坑指南
很多开发者刚用这两个API的时候,经常会遇到一些莫名其妙的bug,大多是踩了这几个常见的坑: 第一个坑是侦听reactive定义的对象时拿不到旧值,如果你直接把reactive定义的对象传给watch作为侦听目标,不管你有没有开deep,回调里拿到的旧值和新值都是同一个引用对象,根本没法做对比,这是因为reactive返回的是响应式代理对象,引用地址没变,Vue不会单独存旧值,要解决这个问题,要么把对象转成ref定义,要么就侦听对象下的具体属性,比如写成`watch(() => user.name, (newVal, oldVal) => {})`,就能拿到正确的旧值。 第二个坑是watchEffect里的异步逻辑收集不到依赖,如果你在watchEffect的回调里写了异步逻辑,比如setTimeout、接口请求,然后在异步回调里才读取响应式数据,那这个数据不会被纳入依赖范围,后续变化的时候也不会触发watchEffect重新执行,因为依赖收集只会在watchEffect同步执行的阶段进行,异步逻辑执行的时候收集阶段已经结束了,要解决这个问题,要么把响应式数据的读取提到异步逻辑外面,要么就换成watch显式声明依赖。 第三个坑是watchEffect的意外依赖导致多余执行,很多人图省事,把很多逻辑都塞到watchEffect里,一不小心在回调里用到了其他无关的响应式数据,比如loading状态、弹窗开关等,就会导致这些无关数据变化的时候,watchEffect也会重新执行,比如本来只是筛选条件变了才拉列表,结果loading变了也重复拉接口,多出很多无用的请求,这种情况就不如用watch显式声明依赖更稳妥。实际项目里到底该怎么选?直接套这个判断逻辑就行
不用纠结两个API哪个更高级,你只需要顺着这几个问题判断,就能快速选出合适的API: 首先问自己第一个问题:你版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


