一、前言
对于一个vue
的老玩家来说,数据的监听可能再熟悉不过了,在平时的开发中,或多或少都会使用watch
和去监听某个属性,而数据的监听也是组件的一项重要工作,在vue3
中,虽然保留了这个API,但是变化还是很大的,而这篇文章,就是要讲解着watch
的变化与使用,希望对你有帮助。
二、watch
对于watch
,vue3
在保留了原来的功能之外,还新增了watchEffect
来帮助我们更简单的进行监听。
回顾vue2
首先先回顾下vue2
中的用法,
在vue2
中,watch
是一个对象,和data
、methods
同级配置:
export default {
data() {
return {
// ...
}
},
// 注意这里,放在 data 、 methods 同个级别
watch: {
// ...
},
methods: {
// ...
}
}
同时,他还有很多类型配置,选项式的API的类型如下:
watch: { [key: string]: string | Function | Object | Array}
联合类型繁多,意味着用法复杂,下面看下官网的例子:
export default {
data() {
return {
a: 1,
b: 2,
c: {
d: 4
},
e: 5,
f: 6
}
},
watch: {
// 侦听顶级 property
a(val, oldVal) {
console.log(`new: ${val}, old: ${oldVal}`)
},
// 字符串方法名
b: 'someMethod',
// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深
c: {
handler(val, oldVal) {
console.log('c changed')
},
deep: true
},
// 侦听单个嵌套 property
'c.d': function (val, oldVal) {
// do something
},
// 该回调将会在侦听开始之后被立即调用
e: {
handler(val, oldVal) {
console.log('e changed')
},
immediate: true
},
// 你可以传入回调数组,它们会被逐一调用
f: [
'handle1',
function handle2(val, oldVal) {
console.log('handle2 triggered')
},
{
handler: function handle3(val, oldVal) {
console.log('handle3 triggered')
}
/* ... */
}
]
},
methods: {
someMethod() {
console.log('b changed')
},
handle1() {
console.log('handle 1 triggered')
}
}
}
说实话,对于初学者来说可能不太友好,有点复杂了,另外,不能用箭头函数来定义watcher
函数,因为箭头函数绑定了父级作用域的上下文,所以this
不能指向组件实例。
同时,vue2
也可以通过this.$watch()
这个API的用法来实现对某个数据的监听,它接受三个参数: source
、callback
和options
。
export default {
data() {
return {
a: 1,
}
},
// 生命周期钩子
mounted() {
this.$watch('a', (newVal, oldVal) => {
// ...
})
}
}
拥抱vue3
在vue3
的组合式API中,watch
是一个函数,可以接受三个参数;
import { watch } from 'vue'
// 一个用法走天下
watch(
source, // 必传,要监听的数据源
callback, // 必传,监听到变化后要执行的回调函数
options // 可选,一些监听选项
)
类型声明
在学习它的用法之前,我们先来对它的TS类型定义有个简单的了解,watch
作为组合式API,根据使用方式有两种类型定义:
- 基础用法的TS类型:
// watch 部分的 TS 类型
// ...
export declare function watch<T, Immediate extends Readonly<boolean> = false>(
source: WatchSource<T>,
cb: WatchCallback<T, Immediate extends true ? T | undefined : T>,
options?: WatchOptions<Immediate>
): WatchStopHandle
- 批量监听的TS类型:
// watch 部分的 TS 类型
// ...
export declare function watch<
T extends MultiWatchSources,
Immediate extends Readonly<boolean> = false
>(
sources: [...T],
cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>,
options?: WatchOptions<Immediate>
): WatchStopHandle
// MultiWatchSources 是一个数组
declare type MultiWatchSources = (WatchSource<unknown> | object)[];
// ...
无论是基础用法还是批量监听,他都接受三个参数:
参数 | 是否必传 | 含义 |
---|---|---|
source | 必传 | 数据源 |
callback | 必传 | 监听到变化要执行的回调函数 |
options | 可选 | 监听选项 |
监听的数据源
为了避免出现监听了但是没反应的情况出现,我们需要先对数据源的TS类型和使用限制做下了解:
// source的 TS 类型
export declare type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
// ...
可以看出,能够用于监听的对象有通过响应式API定义的变量(Ref<T>
),计算数据(computedRef<T>
),和getter
函数(() => T)
。
可以看出,想要成功的监听一个数据,数据源必须是具备响应性的或是一个gette
r,所以只是通let定义的普通变量,是无法监听到他的改变的。
如果是一个响应式的对象,即它本身是响应式的,但是他的prototyp
e不是,如果想要监听他的某个值,就需要写成一个getter
函数,即一个有返回值的函数,返回你要监听的属性,如() => foo.bar
。
回调函数
watch
API的第二个参数是一个回调函数,是监听到变化之后的行为,他的TS类型如下:
export declare type WatchCallback<V = any, OV = any> = (
value: V,
oldValue: OV,
onCleanup: OnCleanup
) => any
// ...
可以看出,回调函数也有三个参数,分别是新值value
,旧值oldValue
,清理函数onCleanup
,这三个参数都可以自己命名,比如把value
换成newValue
什么的。
另外,默认情况下,watch
是惰性的,只有当被监听的数据源发生改变时才执行回调。
介绍完了前两个参数之后,我们可以先来学习下基础用法,也是我们日常中最常用的方法:
<script lang="ts">
import { defineComponent, reactive, watch } from 'vue'
interface UserInfo {
name: string
age: number
}
export default defineComponent({
setup() {
// 定义一个响应式变量
const userInfo: UserInfo = reactive({
name: 'Tom',
age: 18,
})
// 3秒后改变这个变量
setTimeout(() => {
userInfo.name = 'Tony'
}, 3000)
// 监听整个对象
watch(userInfo, (newValue, oldValue) => {
console.log('监听了整个对象 ', newValue)
console.log('监听了整个对象 ', oldValue)
})
// 监听某个属性
watch(
() => userInfo.name,
(newValue, oldValue) => {
console.log('只监听了name:', newValue)
console.log('只监听了name:', oldValue)
}
)
},
})
</script>
可以看到,数据监听成功了,但是你应该发现了,监听对象时,newValue
和oldValue
的值是一样的,因为他们指向的是同一个对象,不仅仅是对象,只要是引用类型,都是如此。
监听多个对象
watch
除了能监听单个数据之外,还可以同时监听多个数据,局别在于他是将多个数据源和回调函数的参数都变成了数组的形式,看下面的例子:
<script lang="ts">
import { defineComponent, reactive, watch, ref } from 'vue'
interface UserInfo {
name: string
age: number
}
export default defineComponent({
setup() {
// 定义两个响应式变量
const userInfo: UserInfo = reactive({
name: 'Tom',
age: 18,
})
const count = ref<number>(0)
// 3秒后改变变量
setTimeout(() => {
userInfo.name = 'Tony'
count.value++
}, 3000)
// 批量监听
watch(
[() => userInfo.name, count],
([newUserName, newCount], [oldUserName, oldCount]) => {
console.log(newUserName, oldUserName)
console.log(newCount, oldCount)
}
)
},
})
</script>
以上就是批量监听的用法了。当我们有多个数据的变化都做同一件事时就可以使用这个功能啦。
监听的选项
讲完了监听的用法之后,我们来讲一下监听的选项,首先介绍在他的TS类型:
export declare interface WatchOptions<Immediate = boolean>
extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
}
// ...
// 继承的 base 类型
export declare interface WatchOptionsBase extends DebuggerOptions {
flush?: 'pre' | 'post' | 'sync'
}
// ...
// 继承的 debugger 选项类型
export declare interface DebuggerOptions {
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
}
watch
的第三个参数是一个对象,可选以下几个选项:
选项 | 类型 | 默认值 | 可选值 | 作用 |
---|---|---|---|---|
deep | boolean | false | true、false | 是否进行深度监听 |
immediate | boolean | false | true、false | 是否立即执行监听回调 |
flush | string | 'pre' | 'pre'、'post'、'sync' | 控制监听回调的调用时机 |
onTrack | (e) => void | 在数据源被追踪时调用 | ||
onTrigger | (e) => void | 在监听回调被触发时调用 |
其中onTrack
和onTrigger
的e
是debugger
事件,建议在回调内方式一个debugger
语句以调试依赖,且这两个选项仅在开发模式有效。
监听选项之deep
deep
选项是一个布尔值,设置为true
时开启深度监听,或者是false
关闭深度监听,看例子:
<script lang="ts">
import { defineComponent, watch, ref } from 'vue'
export default defineComponent({
setup() {
// 定义一个响应式变量
const numbers = ref<number[]>([0])
// 3秒后改变这个变量
setTimeout(() => {
numbers.value.push(1)
console.log('修改后的值:', numbers.value)
}, 3000)
// 监听数组
watch(
numbers,
() => {
console.log('监听后的值:', numbers.value)
},
{
deep: true, // 只有当deep为true时才能监听到数组的改变
}
)
},
})
</script>
上面的例子使用ref
定义了一个数组变量。然后在3s后改变数组的值,你会发现,只有当deep
为true
时才能出发监听,需要注意的是,使用reactive
声明的变量,deep
是默认为true
的。
监听选项之immediate
watch
默认是惰性的,只有当监听的数据发生变化时才会执行回调;
<script lang="ts">
import { defineComponent, watch, ref } from 'vue'
export default defineComponent({
setup() {
// 当immediate为true时触发
const msg = ref<string>('Hello')
// 3秒后改变这个变量
setTimeout(() => {
msg.value = 'Hello vue3'
}, 3000)
// 监听整个对象
watch(
msg,
() => {
console.log('监听后的值:', msg.value)
},
{
immediate: false,
}
)
},
})
</script>
上面这个例子,当immediate
的值为true
时,控制台会打印改变前后的msg
值,就是说,只有当immediate
为true
时,才会在初始化变量的时候就触发回调函数,否则的话默认是只有当数据源变化时才会触发监听。
监听选项之flush
flush选项是用来控制监听回调的调用时机,接受三个指定的字符串,分别是'pre'
、'post'
、'sync'
,默认是'pre'
。
值 | 回调的调用时机 | 使用场景 |
---|---|---|
pre | 将在渲染前被调用 | 允许回调在模板运行前更新了其他值 |
post | 被推迟到渲染之后调用 | 如果要通过ref操作DOM元素或子组件,需要使用改值来启用改选项以达到预期的执行效果 |
sync | 在渲染时同步调用 | 目前来说没什么好处,可以了解但不建议使用 |
停止监听
一般情况下,我们在setup
或者script-setup
中使用watch
的话,组件被卸载的时候watch
监听也会一起被停止,所以一般情况下我们不太需要关心如何停止监听。
但是如果我们需要手动取消监听怎么办呢?比如说我们在一个异步函数里面创建监听函数,这个时候监听函数就不会绑定到当前组件,因此组件卸载的时候就不会一起停止侦听器,这个时候就需要手动停止监听。别担心,vue3
给我们提供了方法。
老规矩,先看下他的TS类型:
export declare type WatchStopHandle = () => void;
用法很简单,如下:
// 定义一个取消观察的变量,它是一个函数
const unwatch = watch(message, () => {
// ...
})
// 在合适的时期调用它,可以取消这个监听
unwatch()
不过需要注意的是,如果你启用了immediate
选项,是不能在第一次触发监听的时候执行它;
<script lang="ts">
import { defineComponent, watch, ref } from 'vue'
export default defineComponent({
setup() {
// 定义一个响应式变量
const msg = ref<string>('Hello')
// 3秒后改变这个变量
setTimeout(() => {
msg.value = 'Hello vue3'
}, 3000)
// 监听整个对象
const unwatch = watch(
msg,
() => {
console.log('监听后的值:', msg.value)
unwatch() // 开启了immediate时不能在这里取消监听,会报错
},
{
immediate: true,
}
)
},
})
</script>
上面的代码会出现如下的报错
Uncaught (in promise) ReferenceError: Cannot access 'unwatch' before initialization
解决的方案也很很简单,可以使用let
并判断变量类型;
import { defineComponent, watch, ref } from 'vue'
import type { WatchStopHandle } from 'vue'
export default defineComponent({
setup() {
// 定义一个响应式变量
const msg = ref<string>('Hello')
// 3秒后改变这个变量
setTimeout(() => {
msg.value = 'Hello vue3'
}, 3000)
// 需要另起一行,并增加类型
let unwatch: WatchStopHandle
unwatch = watch(
msg,
() => {
console.log('监听后的值:', msg.value)
// 加一个判断,是函数才执行它
if (typeof unwatch === 'function') {
unwatch()
}
},
{
immediate: true,
}
)
},
})
</script>
监听效果的清理
在前面介绍回调函数的时候提到过第三个参数,onCleanup
,它可以帮我们注册一个清理函数。
有时候watch
的回调会执行异步操作,当watch
到数据变更的时候,需要取消这些操作,这个时候这个参数就派上用场了,使用场景如下:
watcher
即将重新运行的时候watcher
被停止(组件被现在或者手动停止监听)
TS类型
declare type OnCleanup = (cleanupFn: () => void) => void;
用法也很简单,传入一个回调函数运行即可,不过需要注意的是,需要在停止监听之前注册号清理行为,否则是不会生效的; 我们继续使用上面的例子介绍清理函数的使用方法;
<script lang="ts">
import { defineComponent, watch, ref } from 'vue'
import type { WatchStopHandle } from 'vue'
export default defineComponent({
setup() {
// 定义一个响应式变量
const msg = ref<string>('Hello')
// 3秒后改变这个变量
setTimeout(() => {
msg.value = 'Hello vue3'
}, 3000)
// 需要另起一行,并增加类型
let unwatch: WatchStopHandle
unwatch = watch(
msg,
(newVal, oldVal, onCleanup) => {
console.log('监听后的值:', msg.value)
// 一定要在停止监听函数执行前执行清理函数
onCleanup(() => {
console.log('执行了清理函数。。。')
})
// 然后在停止监听
if (typeof unwatch === 'function') {
unwatch()
}
},
{
immediate: true,
}
)
},
})
</script>
三、watchEffect
在vue3
中,如果我们的某个函数包含了多个需要监听的数据,我们可以使用watchEffect
来简化操作
TS类型
这个API的类型如下,使用的时候需要传入一个副作用函数,相当于watch
的回调函数,也可以传入一些可选的选项。同时它也会返回一个用于停止监听的函数;
export declare type WatchEffect = (onCleanup: OnCleanup) => void
export declare function watchEffect(
effect: WatchEffect,
options?: WatchOptionsBase
): WatchStopHandle
可以看到,他也会传入一个清理回调的参数onCleanup
,用法和上面是一样的;
使用示例
<script lang="ts">
import { defineComponent, watchEffect, ref } from 'vue'
export default defineComponent({
setup() {
// 定义一个响应式变量
const name = ref<string>('Tom')
const age = ref<number>(18)
const userInfo = (): void => {
console.table({
name: name.value,
age: age.value,
})
}
// 3秒后改变name
setTimeout(() => {
name.value = 'Jeck'
}, 3000)
// 4秒后改变age
setTimeout(() => {
age.value = 22
}, 4000)
watchEffect(userInfo)
},
})
</script>
和watch
的区别:
根据上面的例子,我们可以看出他们还是有一定区别的;
-
watch
可以访问侦听数据变化前后的值,而watchEffect
没有; -
watch
是属性变化之后才执行,而watchEffect
则会默认执行一次,然后在变化时执行;
四、总结
以上,便是vue3
中数据监听的用法了,中的来说,相比于vue2
,还是有一个比较大的改变的,功能也更强大了,同时还增加了批量监听的APIwatchEffect
,为我们以后的数据监听带来了很大的方便,同时,数据监听也是我们在日常开发中很常用的功能,所以对于watch的使用,一定要很熟悉才行,文章还是很基础的,需要对大家有用,同时,如果有不对的地方,欢迎大家指正,小弟感激不尽!!!