Vue3 Options API的watch到底怎么用?和Composition API的区别大吗?踩过的坑有哪些?
刚开始用Vue3的时候,很多习惯了Options API的老开发者会有点懵:明明Composition API在setup里写逻辑更自由,但有时候还是怀念旧时代data里直接定义变量、methods里写方法、watch里直接监听数据的“清爽感”,甚至不确定Vue3的Options API watch有没有偷偷变了,还有新手开发者,刚接触框架,对两种API的选择、watch的参数细节都摸不清,写出来的监听要么不生效,要么重复触发,要么监听层级不对,今天咱们就把Vue3 Options API的watch彻底讲透,连容易踩的小陷阱都给你拎出来。
Vue3 Options API watch的基础功能和参数
先从最基础的讲起,Vue3的Options API watch虽然和Vue2有相似之处,但细节上确实优化了一些,比如新增了一些默认值,不过核心逻辑还是没变:用来监听响应式数据的变化,变化时执行自定义的回调函数。
监听单个数据:字符串键或函数
和Vue2一样,Options API里的watch是一个对象,对象的键可以是你要监听的data、computed、props里的变量名(字符串),也可以是返回要监听值的函数;值可以是回调函数、包含配置项的对象、或者methods里的方法名。
先看最简单的字符串键+回调函数的写法:
data() {
return {
userName: '小明',
userAge: 18
}
},
watch: {
// 监听userName的变化
userName(newVal, oldVal) {
console.log(`用户名从${oldVal}变成了${newVal}`)
},
// 监听userAge的变化,用methods里的方法
userAge: 'updateUserInfo'
},
methods: {
updateUserInfo(newVal, oldVal) {
console.log(`年龄从${oldVal}变成了${newVal}`)
}
}
这种写法最常用,但只能监听顶层的响应式变量,如果要监听对象的某个属性、数组的某个元素,或者更复杂的表达式,就需要用返回值的函数形式:
data() {
return {
user: {
name: '小红',
address: {
city: '北京',
district: '朝阳区'
}
},
hobbies: ['读书', '旅行']
}
},
watch: {
// 监听user.address.city的变化
'user.address.city': function(newVal, oldVal) {
console.log(`所在城市从${oldVal}变成了${newVal}`)
},
// 监听user对象的整体变化(浅监听),或者某个计算属性
// 也可以监听数组的长度变化
() => this.user.address.district + this.hobbies.length,
function(newVal, oldVal) {
console.log(`触发了复合条件的变化:${newVal}`)
}
}
这里提一下,虽然Vue3的Proxy代理可以深度追踪对象,但用字符串键直接写嵌套属性(user.address.city')也是完全支持的,不需要像某些早期文章说的那样必须用函数,但函数形式的灵活性更高,比如可以结合其他变量做复合监听。
三个核心配置项:immediate、deep、flush
光有回调函数不够,很多场景需要调整监听的触发时机、深度,这时候就要用包含配置项的对象来写值,回调函数放在handler属性里,这三个配置项是Vue3 watch(包括Composition API的watch)最核心的,也是最容易踩坑的地方。
immediate:组件挂载后立刻触发一次
默认情况下,watch只有在监听的数据首次变化之后才会触发回调,但有时候我们需要在组件刚挂载、数据还是初始值的时候,就执行一次逻辑(比如根据初始的city加载对应的district列表),这时候就可以设置immediate: true。
watch: {
'user.address.city': {
handler(newVal) {
// 这里可以调用接口,根据newVal加载区县
console.log(`加载${newVal}的区县列表`)
},
immediate: true // 组件挂载后立刻执行,此时newVal是初始值'北京',oldVal是undefined
}
}
这里要注意,设置immediate: true的时候,第一次触发回调的oldVal是undefined,因为数据还没有发生过“从旧到新”的变化。
deep:深度监听对象或数组的变化
刚才提到,用字符串键直接写顶层对象(比如user)的话,默认是浅监听——只有当user的引用发生变化(比如重新赋值了一个新的对象:this.user = {name: '小刚'})时才会触发回调,而如果只是修改user里的某个属性(比如this.user.name = '小刚'),或者数组里的某个元素(比如this.hobbies[0] = '跑步')、数组的长度(比如push、pop),浅监听是不会生效的。 这时候就要设置deep: true,开启深度监听,不过深度监听会有性能损耗,因为Vue需要递归遍历整个对象或数组,所以要谨慎使用——如果只需要监听对象的某个属性,直接用'user.name'的字符串键或者对应的函数就行,不要给整个对象开deep。
watch: {
// 浅监听user,只有引用变才触发
user: {
handler(newVal, oldVal) {
console.log('user引用变化了')
}
},
// 深度监听hobbies,数组元素、长度变都触发
hobbies: {
handler(newVal, oldVal) {
console.log('hobbies变化了')
},
deep: true
}
}
还要注意一个细节:开启deep:true之后,修改对象或数组的内容时,回调函数里的newVal和oldVal是同一个引用(因为修改的是同一个对象/数组,没有生成新的引用),所以无法通过对比newVal和oldVal的属性来判断具体哪里变了,如果需要知道具体的变化项,可以用watchEffect(不过Composition API的watchEffect更常用),或者手动在回调里做对比。
flush:调整回调的触发时机
Vue3的响应式更新是批量的——当你在一个tick里修改多个数据时,Vue不会立刻更新DOM,也不会立刻触发所有的watch回调,而是会等到下一个tick再统一执行,这样可以避免重复渲染和计算,提升性能。 但有时候我们需要调整watch回调的触发时机,比如在DOM更新之前执行(比如获取旧DOM的高度),或者在DOM更新之后同步执行(比如获取新DOM的高度),这时候就可以用flush配置项,它有三个可选值:
- 'pre':默认值,在DOM更新之前触发回调
- 'post':在DOM更新之后触发回调
- 'sync':在数据变化同步触发回调(跳过批量更新,可能会影响性能,不推荐频繁使用)
举个例子,比如我们有一个输入框,输入内容后要更新一个文本,同时获取更新后文本的高度:
<template>
<input v-model="inputText" placeholder="输入内容">
<p ref="textRef">{{ inputText }}</p>
</template>
<script>
export default {
data() {
return {
inputText: ''
}
},
watch: {
inputText: {
handler(newVal) {
// 默认flush是pre,此时DOM还没更新,textRef的高度是旧的
console.log('pre触发时的高度:', this.$refs.textRef?.offsetHeight)
}
},
// 另一个监听,用post
'inputText-post': {
// 哦不对,应该直接监听inputText,设置flush: 'post'
// 重新写
inputText: {
handler(newVal) {
// flush是post,此时DOM已经更新,textRef的高度是新的
console.log('post触发时的高度:', this.$refs.textRef?.offsetHeight)
},
flush: 'post'
}
}
}
}
</script>
刚才的例子写错了,一个watch键不能有两个值,要实现这种需求,可以把两个逻辑放在同一个handler里,或者用两个不同的函数键,或者用Composition API的多个watch,不过Options API里更简单的是用同一个handler,里面可以先处理pre的逻辑,然后用nextTick处理post的逻辑——不过有了flush: 'post'就更方便了。
Vue3 Options API watch和Composition API watch的区别
虽然核心逻辑都是监听响应式数据,但两种API的watch在写法、灵活性上还是有一些区别的,老开发者切换的时候要注意,新手也可以根据场景选择合适的API。
写法上的区别
Options API的watch是挂载在组件实例的watch选项里的,键是监听的目标,值是回调或配置对象;而Composition API的watch是在setup函数里直接调用的,可以多次调用,每次调用监听一个或多个目标。 举个对比的例子:
// Options API
export default {
data() {
return {
a: 1,
b: 2
}
},
watch: {
a(newVal) { console.log('a变了', newVal) },
b(newVal) { console.log('b变了', newVal) }
}
}
// Composition API
import { ref, watch } from 'vue'
export default {
setup() {
const a = ref(1)
const b = ref(2)
watch(a, (newVal) => { console.log('a变了', newVal) })
watch(b, (newVal) => { console.log('b变了', newVal) })
// 也可以同时监听多个
watch([a, b], ([newA, newB], [oldA, oldB]) => {
console.log('a或b变了', newA, newB)
})
return { a, b }
}
}
从写法上看,Composition API的watch更灵活,可以同时监听多个目标,而且多个watch之间的逻辑可以写得更近,不需要分开在watch选项里,对于复杂的组件逻辑来说,更容易维护。
监听目标的区别
Options API的watch监听的是组件实例上的属性(data、computed、props),而Composition API的watch监听的是响应式对象本身(ref、reactive、computed,或者返回值的函数)。 这里有个小区别:监听reactive对象的时候,Composition API的watch默认就是深度监听,而Options API的watch监听顶层reactive转换后的data对象,默认还是浅监听。
// Options API
export default {
data() {
return {
user: { name: '小明' } // data里的对象会被Vue3自动用reactive转换
}
},
watch: {
user() {
// 默认浅监听,修改user.name不会触发
console.log('user变了')
}
}
}
// Composition API
import { reactive, watch } from 'vue'
export default {
setup() {
const user = reactive({ name: '小明' })
watch(user, () => {
// 默认深度监听,修改user.name会触发
console.log('user变了')
})
return { user }
}
}
如果不想让Composition API的watch深度监听reactive对象,可以手动设置deep: false。
生命周期的区别
Options API的watch是在组件实例创建之后、挂载之前(如果是immediate: true的话,是在created之后、beforeMount之前)初始化的;而Composition API的watch是在setup函数执行时同步初始化的,如果设置了immediate: true,也是在setup里同步触发的。
取消监听的区别
默认情况下,两种API的watch都会在组件卸载时自动取消监听,不需要手动处理,但如果需要手动提前取消监听,Composition API的watch会返回一个取消函数,直接调用就行;而Options API的watch没有直接返回取消函数,不过可以通过$watch方法手动添加监听,然后获取取消函数。 举个例子:
// Options API手动取消监听
export default {
data() {
return {
a: 1
}
},
mounted() {
// 用$watch手动添加监听,获取取消函数
this.unwatchA = this.$watch('a', (newVal) => {
console.log('a变了', newVal)
if (newVal === 5) {
// 当a变成5时,取消监听
this.unwatchA()
}
})
}
}
// Composition API手动取消监听
import { ref, watch } from 'vue'
export default {
setup() {
const a = ref(1)
const unwatchA = watch(a, (newVal) => {
console.log('a变了', newVal)
if (newVal === 5) {
unwatchA()
}
})
return { a }
}
}
从这个角度看,Composition API的取消监听更方便,因为取消函数直接就在setup里,不需要等到mounted或者其他生命周期。
Vue3 Options API watch的常见踩坑点
讲完了基础功能和区别,咱们来聊聊Vue3 Options API watch最容易踩的几个坑,很多老开发者和新手都中过招。
坑一:直接监听data里的数组/对象,修改属性/元素不触发
刚才在讲deep配置项的时候提到过,这个是最常见的坑,解决办法要么是给对应的watch配置项加上deep: true,要么是直接监听具体的属性/元素,要么是修改数组/对象时生成新的引用(比如用展开运算符:this.user = {...this.user, name: '小刚'};this.hobbies = [...this.hobbies, '摄影'])。
坑二:设置immediate: true时,oldVal是undefined
这个刚才也提到过,不是bug,是正常的设计,解决办法就是在handler里判断一下oldVal是不是undefined,如果是就跳过某些逻辑,或者只执行初始化的逻辑。
坑三:开启deep: true后,newVal和oldVal是同一个引用
这个也是Proxy代理的特性导致的,因为修改的是同一个对象/数组,没有生成新的内存地址,解决办法要么是不要用deep: true,直接监听具体的属性,要么是在handler里手动做深拷贝,对比深拷贝后的对象的属性,要么是用watchEffect。
坑四:监听props时,修改了props的内容
虽然Vue3会在开发模式下给你警告,但生产模式下不会,而且props是父组件传递过来的,子组件不应该直接修改,否则会导致数据流混乱,如果子组件需要修改props的内容,应该通过emit事件通知父组件修改,或者用computed做一个中间层。
坑五:在handler里直接修改监听的数据,导致无限循环
这个也是比较常见的,比如监听a,然后在a的handler里又修改a,就会导致无限循环,解决办法要么是避免直接修改监听的数据,要么是在修改前加一个判断,只有满足某个条件才修改,要么是用watchEffect的stop选项(不过Options API里用$watch的话,可以在修改前调用unwatch,修改后再重新watch,但比较麻烦)。
坑六:监听的目标是普通的非响应式变量
Options API里的watch只能监听data、computed、props里的响应式变量,如果你监听的是在created、mounted或者其他生命周期里直接定义的普通变量(比如this.c = 3,没有放在data里),那watch是不会生效的,因为这个变量不是响应式的,解决办法要么是把变量放在data里,要么是用ref/reactive在Composition API里定义(不过如果坚持用Options API的话,还是放在data里吧)。
什么时候用Vue3 Options API的watch,什么时候用Composition API的?
两种API的watch各有优缺点,具体用哪种可以根据你的项目场景和个人习惯来选择:
- 如果你的项目是从Vue2迁移过来的,而且主要逻辑都还是用Options API写的,那继续用Options API的watch就可以,不用特意改成Composition API的,这样可以减少迁移成本。
- 如果你的项目是新项目,而且组件逻辑比较简单,比如只有几个监听、几个方法,那用Options API的watch也没问题,写法比较直观,适合新手。
- 如果你的项目是新项目,而且组件逻辑比较复杂,比如有很多相关的监听、计算属性、方法,或者需要手动取消监听、同时监听多个目标,那用Composition API的watch会更方便,逻辑可以写得更紧凑,更容易维护。
- 如果你同时用两种API写组件(Vue3支持混合使用),那根据逻辑所在的位置选择就行——如果逻辑在Options API里,就用Options API的watch;如果逻辑在setup里,就用Composition API的watch。
最后再总结一下:Vue3 Options API的watch和Vue2的核心逻辑差不多,新增了一些默认值和优化,三个核心配置项immediate、deep、flush一定要掌握,常见的踩坑点也要注意避免,至于选择哪种API的watch,还是那句话:适合自己的就是最好的。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网


