Vue3中的 computed、watch 和 watchEffect
1.watch
1.1 概念
watch
用于侦听一个或多个响应式数据源,并在数据源发生变化时调用所给出的回调函数。
1.2 用法
实际上,在 Vue2 中,对于watch
的使用是很常见的,所以其基本使用这里就不说了,主要探讨 Vue3 中相较于之前的变化。
在 Vue3 中,原本常用的两种方法,仅适用于用ref
创建的响应式数据。
import { watch } from 'vue'
// 监视ref所定义的一个响应式数据
watch(sum,(newValue,oldValue)=>{
console.log('sum变了',newValue,oldValue)
},{ immediate: true,deep: true })
// 监视ref所定义的多个响应式数据,newValue 和 oldValue都是数组
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变了',newValue,oldValue)
})
结合官方文档,可以总结如下:
- 第一个参数:需要侦听的属性,必须为一个响应式数据。如果不是,则会报错。
- 第二个参数:一个回调函数,即如何处理侦听属性的逻辑代码。
- 第三个参数:可选参数,类型为对象。
watch
可以一次侦听多个响应式属性,只需要将它们放在一个数组中即可。
对于watch
中的第三个参数,其要求传入一个对象,其中包括:
- immediate — 是否立即执行,在 Vue 中,
watch
默认为懒侦听,即仅在侦听对象发生变化时执行回调。该属性默认为false
。当将该属性置为true
时,会在**setup
函数执行阶段**就会调用一次回调函数,且第一次调用时,回调函数中的oldValue
为undefined
。 - deep — 是否开启深度侦听,即当侦听对象为一个具有多层次的响应式对象,其内层属性发生变化时是否能够被侦听到并触发回调。
当侦听对象为一个由ref
处理得到的响应式对象,此时如果想要实现深度侦听,需要手动开启深度解析器deep: true
。例如:
import { ref,watch } from 'vue'
const deepObj = ref({
a: [1,3,5,6,[8,7,9]],
b: '66'
})
setTimeout(()=>{
deepObj.value.a[4][0] = 9
console.log(deepObj.value)
},3000)
watch(deepObj,(newValue,oldValue) => {
console.log(oldValue)
console.log(newValue)
},{ deep: true })
// 这种情况下不加deep: true,无法触发回调函数
// 加deep: true,触发回调函数
这里实际上也可以看到,当侦听一个对象时,无法正确获取到 oldValue。因为 oldValue 和 newValue 实际上都是是对侦听对象的一个引用,所以实际上此时这两者都指向同一个对象,所以输出是相同的,这也是watch
的一个小坑吧。
而当侦听属性是一个由reactive
处理得到的响应式对象的话,就会强制开启深度侦听,此时deep
配置是无效的。
import { watch, reactive } from 'vue'
const deepObj = reactive({
a: [1,3,5,6,[8,7,9]],
b: '66'
})
setTimeout(()=>{
deepObj.a[4][0] = 9
console.log(deepObj)
},3000)
watch(deepObj,(newValue,oldValue) => {
console.log(oldValue)
console.log(newValue)
})
// 没写deep: true,但还是触发了回调函数
// 加上deep: false,你会发现配置无效,回调函数依旧被调用
1.3 flush与回调的触发时机
总所周知,Vue 组件的更新是异步的,当我们所侦听的属性发生变化时,就可能触发 Vue 组件的更新和侦听回调。
而在默认情况下,侦听的回调会发生在 Vue 组件更新之前。
也就是说,此时我们在侦听回调函数中,所能够访问到的 DOM 是 Vue 组件更新之前的状态。
而实际上,我们在watch
的第三个参数中,还可以配置一个属性:flush。通过这个属性,我们可以调整回调函数的调用时机。
flush 可以设定三种值'pre'(默认值)
、‘post’
、‘sync’
。
将其设置为post
,即可实现将侦听回调触发时机改为Vue组件更新之后。
1.4 watch
侦听reactive
处理的响应式数据
需要注意以下两点:
- 此时无法获取 oldValue。
- 强制开启深度监视(deep配置无效)
// 监视reactive所定义的一个响应式数据
watch(data,(newValue,oldValue)=>{
console.log('data变化',newValue,oldValue)
})
// 监视reactive所定义的一个响应式数中的某个属性,此时可以获得 oldValue。
watch(()=>data.name,(newValue,oldValue)=>{
console.log('data变化',newValue,oldValue)
})
// 监视reactive所定义的一个响应式数中的某些属性(同时侦听多个)
watch([()=>data.name,()=>data.age],(newValue,oldValue)=>{
console.log('data变话',newValue,oldValue)
})
但需要注意的是,这里有一个特殊情况:
// 监视reactive所定义的一个响应式数中的对象属性,此时无法获得oldValue,且 deep 配置有效
// 这也是官方文档深度侦听器有介绍的特殊情况
watch(()=>data.job,(newValue,oldValue)=>{
console.log('data变了',newValue,oldValue)
},{deep: true})
2.watchEffect
2.1 描述
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。watchEffect
比watch
更简易易用,但是高度的封装意味着它更抽象,没有watch
好理解。
2.2 用法
import { watchEffect } from 'vue'
// 上来就回调一次
watchEffect(()=>{
const x1 = sum.value
console.log('watchEffect所指定的回调执行了')
})
结合官方文档,总结如下:
- 不用指明监视哪个属性,监视的回调中用到哪个属性,就会自动追踪哪个属性(和
computed
类似) - 非惰性侦听,(与默认的
watch
相反),但是在配置对象中没有像immediate
这样的属性控制惰性或非惰性侦听,这也意味着非惰性侦听这个特性无法被更改。 - 最多只有两个参数,第一个参数为副作用函数(可以理解为回调函数),第二个是可选参数为配置对象,里面属性下面再说。
- 返回值是一个用来停止该副作用的函数。(可以理解为停止侦听器)
对于watchEffect
中的第二个参数,要求传入一个对象,其属性有: flush、onTrack / onTrigger
这里主要说flush — 与watch
中相同,控制回调执行的时机。
当 flush: posh
时,情况和watch
的一样,用于控制回调时机。有更方便的别名,用法 ⬇
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
})
对于flush: sync
,也有更方便的别名,用法⬇
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
})
而对于flush: sync
,官方描述如下:
某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置
flush: 'sync'
来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。
“在响应式依赖发生改变时立即触发侦听器”,可能有朋友对这点会有疑惑,我每次使用watch
、watchEffect
侦听响应式属性,每次在打印台都能马上看到打印,不就是说明立即触发了侦听器的副作用函数吗?其实并非如此,这里有必要解释一下,这里缓存的意义。
缓存的概念相信大家都并不陌生,但是此处Vue中的缓存是指什么呢?
我们都早早的在Vue2时学习了**$nextTick
的意义,以及Vue更新组件的异步渲染**。同样的, Vue3中也有nextTick
这一和$nextTick
意义作用对等的东西,甚至熟悉 JS 事件循环机制的朋友知道 eventloop 的一个循环我们称之为 tick。
熟悉上面概念的朋友,理解这里的缓存就十分轻松了,原来 Vue3 会对watchEffect
侦听器的副作用函数中的响应式依赖数据作缓存处理。
watchEffect
侦听器可能会同时追踪多个响应式数据,当多个响应式数据在同一时间发生变化时,内部会“稍作等待”,观察是否有其他响应式数据发生变化需要触发副作用函数,而最终的结果就是打印台只触发一次副作用函数,反映最终结果。在数据量少、逻辑简单时,“稍作等待”的时间十分短,就会让我们产生“立即触发了侦听器”的错觉。
言归正传,flush: sync
就是希望打破这一缓存等待的机制,让其真正意义上的"立即触发侦听器",所以需要谨慎使用。
2.3 watchEffect
停止侦听器。
在上面总结的第四点中,说到过watchEffect
会返回一个函数,用于停止该副作用函数。
即:
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
那这个函数有哪些应用的场景呢?
什么应用场景呢?
通常watchEffect
需用同步语句创建,他会绑定到当前组件上,在组件销毁时会自动停止侦听,防止内层泄露。同步语句创建其实就是上面的写法,直接在setup中创建注册。
如果用异步语句创建,如用setTimeout函数包裹,则不会自动停止,需要手动停止。
2.4 watchEffect
副作用清除
官方给出的例子如下:
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
主要要理解好 当中onCleanup的意义,当然这只是个参数名你取什么名字都可以,它是一个用于注册副作用清理的回调函数。
该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。一般是做一些异步请求连发限制或取消请求的操作,保证请求数据的完整和准确性。
3.computed
官方对于computed
的描述如下:
接受一个 getter 函数,返回一个只读的响应式 ref 对象。该 ref 通过 .value 暴露 getter 函数的返回值。它也可以接受一个带有 get 和 set 函数的对象来创建一个可写的 ref 对象。
3.1 computed
的特性
- 在 getter 函数中,在追踪的响应式依赖没有发生变化时,返回都会是上次缓存的数据。(计算属性和方法的区别)
- 只有在
computed
中的响应式依赖发生变化时,才会再次进行运算。
4.三者的区别
- 参数上,
computed
只接受一个 getter 函数,watch
和watchEffect
则不同,需要传入回调的函数。 - 返回值,
computed
必须要有个返回值,他会是一个只读的ref
对象(最佳实践场景),watch
和watchEffect
的返回会是一个用于停止侦听的函数。 computed
、watchEffect
都会自动追踪响应式依赖,watch
则需要主动追踪响应式依赖。- 使用上,
computed
着重计算,watch
和watchEffect
着重侦听。从参数上考究,computed
没有像watch
和watchEffect
的配置对象参数去支持调整函数触发时机,官方也说了,在computed
的函数中不能做异步请求或者更改 DOM,而watch
和watchEffect
则相反,十分支持完成类似操作。