最近面试,问到watchwatchEffect的区别,在学习Vue3的路上知道了解过,但是自己的项目用的少或是没有运用到,对watchwatchEffect感受不够深刻,答不上来(真是菜!),写篇文章研究探讨一下。

建议本文章和官方文档一起搭配着看,下面会附链接,不多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)
})

依照官方的说法和描述先作出以下简单总结:

  1. 第一个参数为需要侦听的属性且必须为一个响应式数据(不是会报警告)
  2. 第二个参数为回调函数,就是你要如何处理侦听属性的逻辑代码
  3. 第三个参数是可选参数,是一个对象,下面再详细介绍里面参数意义。
  4. 可以在同一个watch中侦听多个响应式属性,具体做法就是将他们放到一个数组内,可参考上面的代码。

watch中可选参数对象具体属性意义

具体官方描述可以看官方文档,上面有链接,这部分主要大白话讲述。

immediate

是否立即执行,watch默认懒侦听,也就是默认immediate:false,如果把该值改为true,就会在setup函数执行阶段就调用回调函数一次,且第一次调用时旧值是 undefined

deep

深度侦听,表现为当一个有多层次响应式对象,内层属性发生变化时是否能侦听得到,触发回调函数。
官方解释:深度侦听器

当侦听属性是一个是由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,你会发现配置无效,回调函数依旧被调用
flush

我们知道Vue组件更新是异步的,当侦听属性发生变化时,就可能触发Vue组件的更新和侦听回调。默认情况下,侦听回调的触发会在Vue组件更新之前,也就是说,在没有设定flush值的时候,你在侦听回调函数中所能获取到的DOM是Vue组件更新前的状态,设定flush: post,就可以将侦听回调触发时机改为Vue组件更新之后
官方解释:回调的触发时机

flush 可以设定三种值'pre'(默认值)‘post’‘sync’

onTrack / onTrigger

官方解释:响应式调试
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

立即运行一个函数,同时响应式地追踪其依赖,并在依赖更改时重新执行。watchEffectwatch更简易易用,但是高度的封装意味着它更抽象,没有watch好理解。

官方链接 ➡ Vue3中的watchEffect

先上代码,看用法 ⬇

import { watchEffect } from 'vue'
// 上来就回调一次
watchEffect(()=>{
    const x1 = sum.value
    console.log('watchEffect所指定的回调执行了')
})

依照官方的说法和描述先作出以下简单总结:

  1. 不用指明监视哪个属性,监视的回调中用到哪个属性,就会自动追踪哪个属性(和computed类似)
  2. 非惰性侦听,(与默认的watch相反),但是在配置对象中没有像immediate这样的属性控制惰性或非惰性侦听,这也意味着非惰性侦听这个特性无法被更改。
  3. 最多只有两个参数,第一个参数为副作用函数(可以理解为回调函数),第二个是可选参数为配置对象,里面属性下面再说。
  4. 返回值是一个用来停止该副作用的函数。(可以理解为停止侦听器)

watchEffect中可选参数对象具体属性意义

主要有三个属性 ➡ flush、onTrack / onTrigger
大部分属性一样和watch的相同,下面解释watch时没讲到的。

flush

flush: posh时,情况和watch的一样,用于控制回调时机。有更方便的别名,用法 ⬇

import { watchPostEffect } from 'vue'

watchPostEffect(() => {
  
})

flush: sync

也有更方便的别名,用法⬇

import { watchSyncEffect } from 'vue'

watchSyncEffect(() => {
  
})

先给段官方描述 ⬇

某些特殊情况下 (例如要使缓存失效),可能有必要在响应式依赖发生改变时立即触发侦听器。这可以通过设置 flush: 'sync' 来实现。然而,该设置应谨慎使用,因为如果有多个属性同时更新,这将导致一些性能和数据一致性的问题。

“在响应式依赖发生改变时立即触发侦听器”,可能有朋友对这点会有疑惑,我每次使用watch、watchEffect侦听响应式属性,每次在打印台都能马上看到打印,不就是说明立即触发了侦听器的副作用函数吗?其实并非如此,这里有必要解释一下,这里**“缓存”**的意义(严格意义上来说,watchEffect并没有缓存,这里只是把这种类似的情况比喻成“缓存”便于理解,computed才有缓存)
缓存的概念相信大家都并不陌生,但是此处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的意义,当然这只是个参数名你取什么名字都可以,它是一个用于注册副作用清理的回调函数。该回调函数会在副作用下一次重新执行前调用,可以用来清除无效的副作用,例如等待中的异步请求。
一般是做一些异步请求连发限制或取消请求的操作,保证请求数据的完整和准确性。

总结

watchwatchEffect区别

参数上
watch接收三个参数,watchEffect接收两个参数,因为watchEffect会自动追踪副作用函数中的响应式数据。

执行上
watch默认懒侦听,可以通过第三个参数配置对象中的 immediate 属性修改,watchEffect强制非惰性侦听,且参数配置对象中没有 immediate 属性修改。

副作用函数上的参数
watch回调函数可接收三个参数新值、旧值,以及一个用于注册副作用清理的回调函数;watchEffect回调函数只接受一个用于注册副作用清理的回调函数。

最后给个官方链接 ➡ 从这开始看

关于computedwatchwatchEffect的区别 ➡ Vue3中的computed、watch和watchEffect的区别

文章有问题之处还望评论斧正!

Logo

旨在为数千万中国开发者提供一个无缝且高效的云端环境,以支持学习、使用和贡献开源项目。

更多推荐