最近面试,问到
watch
和watchEffect
的区别,在学习Vue3的路上知道了解过,但是自己的项目用的少或是没有运用到,对watch
和watchEffect
感受不够深刻,答不上来(真是菜!),写篇文章研究探讨一下。
建议本文章和官方文档一起搭配着看,下面会附链接,不多bb,直入主题!
watch
概念:侦听一个或多个响应式数据源,并在数据源变化时调用所给的回调函数。
下面提到的名词或者概念,都由Vue3官方文档的watch
介绍可以看到,这里附一个链接,方便各位朋友看官方最准确的描述:Vue3中的watch
先上代码,看用法(watch
部分最后再补充使用reactive
响应式数据的写法) ⬇
以下两种情况只适用于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:false
,如果把该值改为true,就会在setup函数执行阶段就调用回调函数一次,且第一次调用时旧值是 undefined
。
深度侦听,表现为当一个有多层次响应式对象,内层属性发生变化时是否能侦听得到,触发回调函数。
官方解释:深度侦听器
当侦听属性是一个是由ref
处理的响应式对象的话,就需要手动开启深度解析器deep: true
,否则watch
无法侦听到里面属性的变化,给个测试用例 ⬇
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的值和newValue1的值是一致的,因为他们是同一个对象
当侦听属性是一个是由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,你会发现配置无效,回调函数依旧被调用
我们知道Vue组件更新是异步的,当侦听属性发生变化时,就可能触发Vue组件的更新和侦听回调。默认情况下,侦听回调的触发会在Vue组件更新之前,也就是说,在没有设定flush
值的时候,你在侦听回调函数中所能获取到的DOM是Vue组件更新前的状态,设定flush: post
,就可以将侦听回调触发时机改为Vue组件更新之后。
官方解释:回调的触发时机
flush 可以设定三种值'pre'(默认值)
、‘post’
、‘sync’
官方解释:响应式调试
onTrack
将在响应属性或引用作为依赖项被跟踪时被调用。
onTrigger
将在侦听器回调被依赖项的变更触发时被调用。
调试钩子 只在开发模式下工作
补充,使用watch
侦听reactive
处理的响应式数据写法
// 监视reactive所定义的一个响应式数据
1. 注意此处无法确认的获取oldValue,ref定义的响应式数据没有这个问题
2. 强制开启深度监视(deep配置无效)
watch(data,(newValue,oldValue)=>{
console.log('data变化',newValue,oldValue)
})
// 监视reactive所定义的一个响应式数中的某个属性
watch(()=>data.name,(newValue,oldValue)=>{
console.log('data变化',newValue,oldValue)
})
// 监视reactive所定义的一个响应式数中的某些属性(同时侦听多个)
watch([()=>data.name,()=>data.age],(newValue,oldValue)=>{
console.log('data变话',newValue,oldValue)
})
// 监视reactive所定义的一个响应式数中的对象属性,此时 deep 配置有效
// 这也是官方文档深度侦听器有介绍的特殊情况,上面有链接
watch(()=>data.job,(newValue,oldValue)=>{
console.log('data变了',newValue,oldValue)
},{deep: true})
watchEffect
立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。watchEffect
比watch
更简易易用,但是高度的封装意味着它更抽象,没有watch
好理解。
官方链接 ➡ Vue3中的watchEffect
先上代码,看用法 ⬇
import { watchEffect } from 'vue'
// 上来就回调一次
watchEffect(()=>{
const x1 = sum.value
console.log('watchEffect所指定的回调执行了')
})
依照官方的说法和描述先作出以下简单总结:
watch
相反),但是在配置对象中没有像immediate
这样的属性控制惰性或非惰性侦听,这也意味着非惰性侦听这个特性无法被更改。watchEffect
中可选参数对象具体属性意义主要有三个属性 ➡ flush、onTrack / onTrigger
大部分属性一样和watch
的相同,下面解释watch
时没讲到的。
当 flush: posh
时,情况和watch
的一样,用于控制回调时机。有更方便的别名,用法 ⬇
import { watchPostEffect } from 'vue'
watchPostEffect(() => {
})
flush: sync
也有更方便的别名,用法⬇
import { watchSyncEffect } from 'vue'
watchSyncEffect(() => {
})
先给段官方描述 ⬇
某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置
flush: 'sync'
来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。
“在响应式依赖发生改变时立即触发侦听器”,可能有朋友对这点会有疑惑,我每次使用watch、watchEffect
侦听响应式属性,每次在打印台都能马上看到打印,不就是说明立即触发了侦听器的副作用函数吗?其实并非如此,这里有必要解释一下,这里缓存的意义。
缓存的概念相信大家都并不陌生,但是此处Vue中的缓存是指什么呢?
我们都早早的在Vue2时知道了$nextTick
的意义,知道了Vue更新组件的异步渲染,知道Vue3中也有nextTick
和$nextTick
意义作用对等的东西,甚至熟悉 JS 事件循环机制的朋友知道eventloop一个循环我们称之为tick
。
熟悉上面概念的朋友,理解这里的缓存就十分轻松了,原来Vue3会对watchEffect
侦听器的副作用函数响应式依赖数据作缓存处理,watchEffect
侦听器可能会同时追踪多个响应式数据,当多个响应式数据在同一时间发生变化时,内部会“稍作等待”,观察是否有其他响应式数据发生变化需要触发副作用函数,而最终的结果就是打印台只触发一次副作用函数,反映最终结果。在数据量少、逻辑简单时,“稍作等待”的时间十分短,就会让我们产生“立即触发了侦听器的错觉”。
言归正传,flush: sync
就是希望打破这一缓存等待的机制,让其真正意义上的"立即触发侦听器",谨慎使用。下面给出测试实例 ⬇
import { ref,watchEffect } from 'vue'
const number1 = ref(1)
const number2 = ref(10)
// 点击按钮触发此方法
const upNumber = () => {
number1.value++
number2.value++
}
watchEffect(()=>{
console.log(number1.value)
console.log(number2.value)
})
// 没有添加 flush: 'sync',副作用函数只触发一次
// 添加 flush: 'sync',副作用函数触发两次,因为追踪的两个响应式数据都发生了变化
watchEffect
停止侦听器。上面简单总结第四点,watchEffect
会返回一个函数,用于停止该副作用函数
const stop = watchEffect(() => {})
// 当不再需要此侦听器时:
stop()
什么应用场景呢?
通常watchEffect
需用同步语句创建,他会绑定到当前组件上,在组件销毁时会自动停止侦听,防止内层泄露。同步语句创建其实就是上面的写法,直接在setup中创建注册。
如果用异步语句创建,如用setTimeout函数包裹,则不会自动停止,需要手动停止。
watchEffect
副作用清除这里官方给的例子可太抽象了。⬇
watchEffect(async (onCleanup) => {
const { response, cancel } = doAsyncWork(id.value)
// `cancel` 会在 `id` 更改时调用
// 以便取消之前
// 未完成的请求
onCleanup(cancel)
data.value = await response
})
主要要理解好 当中onCleanup
的意义,当然这只是个参数名你取什么名字都可以,它是一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
一般是做一些异步请求连发限制或取消请求的操作,保证请求数据的完整和准确性。
watch
和watchEffect
区别参数上
watch
接收三个参数,watchEffect
接收两个参数,因为watchEffect
会自动追踪副作用函数中的响应式数据。
执行上
watch
默认懒侦听,可以通过第三个参数配置对象中的 immediate 属性修改,watchEffect
强制非惰性侦听,且参数配置对象中没有 immediate 属性修改。
副作用函数上的参数
watch
回调函数可接收三个参数新值、旧值,以及一个用于注册副作用清理的回调函数;watchEffect
回调函数只接受一个用于注册副作用清理的回调函数。
最后给个官方链接 ➡ 从这开始看
关于computed
、watch
和watchEffect
的区别 ➡ Vue3中的computed、watch和watchEffect的区别
文章有问题之处还望评论斧正!