Vue3 TypeScript 开发中 Watch 怎么用才顺畅?避坑、技巧全讲透
写Vue3项目配TypeScript,是现在前端开发的主流选择,能在代码阶段就拦截不少类型错误,提高后续维护效率,但很多刚上手的同学会发现,Watch在JS里用得好好的,到TS里要么类型报错满天飞,要么得写一堆冗余的类型断言,完全没体会到TS+Watch的爽感,别着急,这篇就从基础到进阶,把Vue3 TypeScript下的Watch所有核心点、避坑指南和实用技巧都捋清楚。
基础用法:让TypeScript自动推导出监听值的类型
很多人一开始写Vue3+TS的Watch,可能会犯一个错误:直接照搬JS的写法,然后发现回调函数里的参数类型是any,根本起不到类型检查的作用,其实只要我们用对了组合式API的导出方式,TypeScript就能完美推导出监听值的类型,完全不需要手动写。
先看组合式API最常用的ref和reactive两种数据类型的监听示例。 如果是监听单个ref值,直接传ref对象就行:
import { ref, watch } from 'vue'
// 定义一个带类型的ref
const count = ref<number>(0)
// 直接把count传给watch
watch(count, (newVal, oldVal) => {
// 这里newVal和oldVal自动被推导为number类型,连自动补全都会提示number的方法
console.log(newVal + oldVal)
})
要是ref里存的是复杂对象呢?比如一个User类型的对象:
import { ref, watch } from 'vue'
// 先定义好User的接口
interface User {
id: number
name: string
age?: number
}
const user = ref<User>({ id: 1, name: '张三' })
// 监听user,回调的参数类型也是User
watch(user, (newUser, oldUser) => {
// 这里能自动补全id、name、age,age还会提示可选属性,不能直接操作
console.log(newUser.id + newUser.name)
if (newUser.age) {
console.log(newUser.age + 1)
}
})
接下来是reactive,reactive本身就是用来存响应式对象的,TypeScript也能自动推导:
import { reactive, watch } from 'vue'
interface User {
id: number
name: string
hobbies: string[]
}
const user = reactive<User>({ id: 2, name: '李四', hobbies: ['篮球'] })
// 直接监听user对象,或者监听user的某个属性
watch(user, (newUser, oldUser) => {
console.log(newUser.hobbies.join(','))
})
// 监听单个属性的话,建议用getter函数,这样不会触发深层监听的问题
watch(() => user.name, (newName, oldName) => {
console.log(newName)
})
这里要提一句,为什么监听reactive的单个属性要用getter函数?如果直接传user.name的话,传进去的是普通字符串,不是响应式的引用,Watch根本不会触发;只有把它包装成返回响应式值的函数,Watch才能监听它的变化。
多个监听源:TypeScript会联合推导出参数类型
除了单个监听源,Vue3还支持监听多个源,不管是ref、reactive的getter,还是普通的返回值函数,都可以放一个数组里,这时候TypeScript会自动把每个监听源的类型联合起来,作为回调函数参数的类型。
举个例子,同时监听count(number类型的ref)和user.name(string类型的getter):
import { ref, reactive, watch } from 'vue'
interface User {
id: number
name: string
}
const count = ref<number>(0)
const user = reactive<User>({ id: 3, name: '王五' })
// 多个监听源放在数组里
watch([count, () => user.name], ([newCount, newName], [oldCount, oldName]) => {
// 这里newCount和oldCount是number,newName和oldName是string
console.log(`count从${oldCount}变成了${newCount}, name从${oldName}变成了${newName}`)
})
这种联合推导的方式非常方便,完全不需要手动指定回调函数的参数类型。
深层监听:注意TypeScript的类型稳定性,避免误操作
深层监听是Watch里常用的一个配置项,用来监听对象或者数组内部的变化,但在TypeScript下用深层监听,有几个容易踩的坑。
第一个坑是:监听ref的复杂对象时,如果不加deep:true,只有整个对象的引用变了才会触发,内部属性变了不会触发;如果加了deep:true,回调函数里的oldVal就和newVal一样了,不管是ref还是reactive都是如此,这个是Vue3本身的机制,和TypeScript没关系,但很多同学容易混淆。
第二个坑是:监听reactive的深层属性时,有时候会出现类型不明确的情况,不过只要我们一开始给reactive指定了正确的接口,这个问题就不会发生。
第三个坑也是最容易被忽略的:如果我们监听的是一个any类型的ref或者reactive,加了deep:true后,TypeScript会失去对内部属性的类型检查,所以尽量不要用any,所有数据都要先定义好接口。
给大家看一个正确的深层监听示例:
import { ref, watch } from 'vue'
interface Hobby {
id: number
name: string
level: number
}
interface User {
id: number
name: string
hobbies: Hobby[]
}
const user = ref<User>({
id: 4,
name: '赵六',
hobbies: [{ id: 1, name: '足球', level: 3 }]
})
// 加deep:true监听内部hobbies数组的变化
watch(user, (newUser) => {
// 这里newUser.hobbies[0].level的类型还是number,不会有问题
console.log(newUser.hobbies[0].level + 1)
}, { deep: true })
// 测试一下,修改hobbies数组里的level,会触发watch
user.value.hobbies[0].level = 5
立即执行:immediate配置下,oldVal的类型可能会有问题
immediate是另一个常用的配置项,用来让Watch在组件挂载后立即执行一次,但在TypeScript下,immediate配置会带来一个类型上的小问题:第一次执行的时候,oldVal是undefined,但TypeScript默认不会给oldVal加undefined类型,这时候如果直接操作oldVal,就会报类型错误。
怎么解决这个问题呢?有两种方法: 第一种是给回调函数的参数手动加undefined类型,
import { ref, watch } from 'vue'
const count = ref<number>(0)
watch(count, (newVal: number, oldVal: number | undefined) => {
// 第一次执行oldVal是undefined,所以要先判断
if (oldVal !== undefined) {
console.log(newVal - oldVal)
}
}, { immediate: true })
第二种是用可选链操作符,但可选链只能用来访问属性,不能用来做运算,所以第一种方法更通用。
泛型Watch:手动指定监听源和回调的类型,增强类型控制
一般情况下,TypeScript的自动推导已经够用了,但有些特殊场景,比如监听源是一个返回值类型不明确的函数,或者我们想对回调函数的参数做更严格的类型控制,这时候就可以用泛型Watch。
Vue3的Watch函数支持两个泛型参数,第一个是监听源的类型,第二个是回调函数的参数类型(可选),不过通常我们只需要指定第一个泛型参数,第二个TypeScript会自动根据第一个来推导。
举个例子,监听一个从接口获取数据的异步函数的返回值(这里用一个模拟的异步函数):
import { ref, watch, onMounted } from 'vue'
// 定义接口返回的数据类型
interface ApiResponse {
code: number
data: {
id: number string
}
message: string
}
// 模拟异步获取数据的函数
const fetchData = async (): Promise<ApiResponse> => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
code: 200,
data: { id: 1, title: 'Vue3+TS教程' },
message: '成功'
})
}, 1000)
})
}
// 定义一个ref来存储异步函数的返回值,类型是ApiResponse | null
const apiData = ref<ApiResponse | null>(null)
// 组件挂载后获取数据
onMounted(async () => {
apiData.value = await fetchData()
})
// 用泛型Watch指定监听源的类型是ApiResponse | null
watch<ApiResponse | null>(apiData, (newData, oldData) => {
if (newData?.code === 200) {
console.log(newData.data.title)
}
})
这里的泛型参数其实也可以省略,因为apiData已经指定了类型,但加上之后可以更明确地告诉TypeScript我们的意图,增强代码的可读性。
和WatchEffect的TS对比:什么时候用Watch,什么时候用WatchEffect
很多同学会分不清Watch和WatchEffect的区别,尤其是在TypeScript下,其实它们的核心区别在JS和TS下是一样的:Watch需要明确指定监听源,只有监听源变化时才会触发;WatchEffect不需要明确指定监听源,它会自动追踪函数内部用到的响应式数据,只要用到的响应式数据变化了就会触发。
在TypeScript下,WatchEffect还有一个优势:函数内部的响应式数据类型都是自动推导的,和Watch一样;但WatchEffect不需要处理immediate配置,因为它默认就是立即执行的;不过WatchEffect没有oldVal,只有newVal的效果(函数内部用到的响应式数据都是最新的),这一点要注意。
举个对比的例子:
import { ref, watch, watchEffect } from 'vue'
const count = ref<number>(0)
const user = ref<User>({ id: 5, name: '孙七' })
// Watch的写法:明确指定监听源,有newVal和oldVal
watch([count, () => user.value.name], ([newCount, newName], [oldCount, oldName]) => {
console.log('Watch触发', newCount, newName, oldCount, oldName)
}, { immediate: true })
// WatchEffect的写法:自动追踪count和user.value.name,没有oldVal
watchEffect(() => {
console.log('WatchEffect触发', count.value, user.value.name)
})
// 测试一下
count.value++
user.value.name = '周八'
这个例子里,Watch和WatchEffect的触发时机基本是一样的(除了Watch第一次执行时的oldVal是undefined),但Watch有oldVal,适合需要对比前后值的场景;WatchEffect没有oldVal,代码更简洁,适合不需要对比前后值,只需要在响应式数据变化时执行操作的场景。
实用避坑指南
最后给大家总结几个Vue3 TypeScript下Watch最容易踩的坑,一定要记牢:
- 监听reactive的单个属性,必须用getter函数,不能直接传属性值。
- 加deep:true监听对象或数组时,oldVal和newVal是一样的,不能用来对比前后值。
- 加immediate配置时,oldVal第一次是undefined,必须先判断再操作。
- 尽量不要用any类型的ref或reactive,所有数据都要先定义好接口,否则TypeScript会失去对内部属性的类型检查。
- 监听多个源时,回调函数的参数是一个数组,顺序和监听源的顺序一致。
Vue3 TypeScript下的Watch其实并不难,只要掌握了基础用法的自动推导、getter函数监听reactive的单个属性、deep和immediate配置的注意事项,还有泛型Watch的特殊场景用法,就能让Watch和TypeScript配合得非常顺畅,既享受到Vue3响应式的便捷,又享受到TypeScript类型检查的安全,希望这篇文章能帮到正在学习Vue3+TS的同学,大家有什么问题也可以在评论区留言讨论。
版权声明
本文仅代表作者观点,不代表Code前端网立场。
本文系作者Code前端网发表,如需转载,请注明页面地址。
code前端网

