彻底搞懂Vue3中的watch函数,功能太强了!

互联架构唠唠嗑 2024-03-21 13:25:04
前言

在 Vue2 和 Vue3 中,侦听器 watch 的用法有些许不同,对于刚从 Vue2 转为 Vue3 的小伙伴来说,可能还不太适应。在本文中,我将详细为大家介绍 Vue3 中 watch 的用法和具体作用。

watch API介绍

watch(source, cb, options)

watch 共接收3个参数,下面一起看看这3个参数都有什么作用:

source:需要侦听的响应式属性,这个属性可以是不同形式的“数据源”,例如:可以是一个 ref (包括计算属性)、可以是一个响应式对象、可以是一个 getter 函数、或多个数据源组成的数组。cb:回调函数。当侦听的响应式属性发生变化时,会触发这个回调函数,它也有3个参数:newValue:响应式属性变化后的值(新值)、oldValue:响应式属性变化前的值(旧值)、onInvalidate:该函数用于清除副作用。options:immediate:是否在页面进入时就触发侦听器,值是一个布尔类型 true/false(默认false)。deep:是否开启深层侦听。值是一个布尔类型 true/false(默认false)。flush:值有3个,'pre' | 'post' | 'sync'(默认是 pre)。pre:指定的回调应该在渲染前被调用、post:可以用来将回调推迟到渲染之后的。如果回调需要通过 $refs 访问更新的 DOM 或子组件,那么则使用该值、sync:如果值设置为 sync,一旦值发生了变化,回调将被同步调用(少用,影响性能)。基本使用

在 Vue3 中,组合式 API 中的 watch 的作用和 Vue2 中选项式 API 的 watch 作用是一样的,它们都是用来侦听响应式状态的变化。无论是在 Vue2 还是 Vue3 中,当响应式状态发生变化时,都会触发一个回调函数。

下面我们一起来看一个简单的例子:

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' // 使用响应式 ref 定义一个变量 count,初始值为 1 let count = ref(1) // 定义一个方法 changeCount,该方法用于按钮点击时更改 count 的值 const changeCount = () => { count.value++ } // 使用 watch 函数侦听响应式 count 值的变化,并在控制台中输出修改前后的值 watch(count, (newValue, oldValue) => { console.log(`新值:${newValue}`) console.log(`旧值:${oldValue}`) })</script><template> <div id="home"> <p>count值:{{ count }}</p> <button @click="changeCount">修改count值</button> </div></template>

上面这段代码中:

我们使用响应式 ref 定义了一个变量 count,其初始值为 1,然后定义了一个方法 changeCount,该方法用于按钮点击时更改 count 的值。然后使用 watch 函数侦听响应式 count 变量值的变化,当 count 变量的值发生变化时,watch 回调函数将被执行,并将新值和旧值作为参数传递给该函数。当我们点击按钮时,控制台会输出如下的内容。

watch侦听器数据源类型

前面我们提到过,watch 的第一个参数 source 可以是不同形式的响应式“数据源”:它可以是一个 ref (包括计算属性)、一个响应式对象、一个 getter 函数、或多个数据源组成的数组。

那么,当我们侦听的数据源不是响应式的数据时,控制台会抛出如下图所示的警告:

侦听ref和计算属性<script setup> // 引入 vue 提供的 ref、watch 和 computed API import { ref, watch, computed } from 'vue' // 使用 ref 定义一个变量 count,初始值为 1 let count = ref(1) // 使用 computed 定义一个计算属性 rideCount,返回 count 之间的乘积 let rideCount = computed(() => { return count.value * count.value }) // 定义一个方法 changeCount,改方法用于按钮点击时更改 count 的值,当 count 的值发生变化时,计算属性 rideCount 的值也会重新计算得到新的结果 const changeCount = () => { count.value++ } // 使用 watch 函数侦听响应式 count 值的变化 watch(count, (newValue, oldValue) => { console.log(`count新值:${newValue}`, `count旧值:${oldValue}`) }) // 使用 watch 函数侦听计算属性 rideCount 值的变化 watch(rideCount, (newValue, oldValue) => { console.log(`rideCount新值:${newValue}`, `rideCount旧值:${oldValue}`) })</script><template> <div id="home"> <p>count值:{{ count }}</p> <p>rideCount值:{{ rideCount }}</p> <button @click="changeCount">修改count值</button> </div></template>

上面这段代码中:

我们使用响应式 ref 定义了一个变量 count,其初始值为 1,然后使用 computed 定义一个计算属性 rideCount,返回两个 count 之间的乘积。接着我们定义了一个方法 changeCount,该方法用于按钮点击时更改 count 的值。当 count 值发生变化时,计算属性 rideCount 的值也会重新计算得到新的结果。然后使用 watch 函数侦听响应式 count 变量值和计算属性 rideCount 值的变化,当我们点击按钮时,count 值会依次加 1,当 count 变量的值发生变化时,watch 回调函数将被执行,同时也可以侦听到计算属性的变化。

侦听getter函数<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' // 使用 ref 定义两个变量 num1 和 num2,初始值都为 0 let num1 = ref(0) let num2 = ref(0) // 定义两个方法 changeNum1 和 changeNum2 // 点击“修改num1的值”按钮时,num1 的值加 5,点击“修改num2的值”按钮时,num2 的值加 10 const changeNum1 = () => { num1.value += 5 } const changeNum2 = () => { num2.value += 10 } // 使用 watch 函数侦听 getter 函数的变化 watch( () => num1.value + num2.value, (newValue, oldValue) => { console.log(`两数之和新:${newValue}`, `两数之和旧:${oldValue}`) } )</script><template> <div id="home"> <p>num1: {{ num1 }}</p> <p>num2:{{ num2 }}</p> <button @click="changeNum1">修改num1的值</button> <button @click="changeNum2">修改num2的值</button> </div></template>

上面这段代码中:

我们使用 ref 定义两个变量 num1 和 num2,初始值都为 0然后定义了两个方法 changeNum1 和 changeNum2,当点击“修改num1的值”按钮时,num1 的值加 5,当点击“修改num2的值”按钮时,num2 的值加 10然后使用 watch 函数侦听 getter 函数的变化,getter 函数返回的是 num1 和 num2 相加的值,当 num1 和 num2 两个值其中一个发生变化时,都会执行 watch 侦听器中的回调函数。

侦听响应式对象

我们都知道,在 Vue3 中定义响应式对象的数据可以用 ref 或 reactive,那么,在使用 watch 侦听响应式对象时,这里就会有两种情况:

侦听reactive声明的响应式对象<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' // 使用 reactive 定义一个响应式对象 person let person = reactive({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changeName,用于更改 person 对象中的 name 属性值 const changeName = () => { person.name = 'Steven' } // 定义一个方法 changeCity,用于更改 person 对象中,address 对象中的 city 属性值 const changeCity = () => { person.address.city = 'San Francisco' } // 使用 watch 函数侦听响应式对象 person 的变化 watch(person, (newValue, oldValue) => { console.log(newValue, oldValue) })</script><template> <div id="home"> <p>name: {{ person.name }}</p> <p>city: {{ person.address.city }}</p> <button @click="changeName">修改name</button> <button @click="changeCity">修改city</button> </div></template>

上面这段代码中:

我们使用 reactive 定义一个响应式对象 person,里面有 name 和 address 属性,其中 address 是一个对象,包含了 city 属性。然后定义了两个方法 changeName 和 changeCity,changeName 方法用于更改 person 对象中的 name 属性值, changeCity 方法用于更改 person 对象中,address 对象中的 city 属性值。然后使用 watch 函数侦听响应式对象 person 的变化,当我们不管是点击“修改name”还是“修改city”按钮时,都会执行 watch 侦听器中的回调函数。

从上面可以看到:不管是 name 还是 city 的值发生变化,都会触发 watch 函数,因此我们可以得出两个结论:

当我们侦听的是用 reactive 声明的响应式对象时,修改响应式对象的任何属性,都会触发 watch 函数。当侦听的响应式数据是 Proxy 类型时,deep 配置项无效,无论设置成 true 还是 false,都会进行深度监听。

然而我们发现有一个问题,在控制台中打印出来的新旧值是一样的,这是什么原因?

这是由于我们定义的 person 数据是通过响应是 reactive 定义的,reactive返回的数据是 Proxy 类型的,由于 newValue 和 oldValue 是同一个地址引用,所以属性值也是一样的。

上面我们侦听的是由 reactive 定义的 person 对象,也就是 Proxy 对象,当我们监听的是 Proxy 对象中的某个属性时,又会是什么情况?

首先,当侦听的属性是简单数据类型时:

<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' // 使用 reactive 定义一个响应式对象 person let person = reactive({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changeName, const changeName = () => { person.name = 'Steven' } // 定义一个方法 changeCity const changeCity = () => { person.address.city = 'San Francisco' } // 使用 watch 函数侦听 name 属性的变化 watch( () => person.name, (newValue, oldValue) => { console.log(`新name:${newValue}`, `旧name:${oldValue}`) } )</script><template> <div id="home"> <p>name: {{ person.name }}</p> <p>city: {{ person.address.city }}</p> <button @click="changeName">修改name</button> <button @click="changeCity">修改city</button> </div></template>

上面这段代码中:

我们使用 watch 侦听器侦听 person 对象中的 name 属性。name 属于简单数据类型。当我们点击“修改name”按钮时,newValue 的值为“steven”,oldValue 的值为 “Echo”,当我们点击“修改city”按钮时,不会再触发 watch 函数。

需要注意的是:此时的 watch 函数中的第一个参数应当是一个箭头函数。

因此我们可以得出一个结论:

当我们侦听响应式对象中的某个属性时,只有当响应式对象的被侦听属性发生变化时,才会触发 watch 方法,其他属性变化不会触发 watch 方法。

当侦听的属性为复杂数据类型时:

<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' // 使用 reactive 定义一个响应式对象 person let person = reactive({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changeName, const changeName = () => { person.name = 'Steven' } // 定义一个方法 changeCity const changeCity = () => { person.address.city = 'San Francisco' } // 使用 watch 函数侦听 address 属性的变化 watch( () => person.address, (newValue, oldValue) => { console.log(`新name:${newValue}`, `旧name:${oldValue}`) } )</script><template> <div id="home"> <p>name: {{ person.name }}</p> <p>city: {{ person.address.city }}</p> <button @click="changeName">修改name</button> <button @click="changeCity">修改city</button> </div></template>

上面这段代码中:

我们使用 watch 侦听器侦听 person 对象中的 address 对象,address 属于复杂数据类型。当我们点击“修改name”或者点击“修改city”按钮时,我们可以看到页面的值被更改了,但控制台一次都不输出。

我们不妨设想一下,改 address 里面的 city 属性,不会触发 watch 函数,那直接更改 address 呢?

<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' // 使用 reactive 定义一个响应式对象 person let person = reactive({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changeName, const changeName = () => { person.name = 'Steven' } // 定义一个方法 changeCity const changeCity = () => { person.address = { city: 'ShangHai' } } // 使用 watch 函数侦听 address 属性的变化 watch( () => person.address, (newValue, oldValue) => { console.log(`新:${JSON.stringify(newValue)}`) console.log(`旧:${JSON.stringify(oldValue)}`) } )</script><template> <div id="home"> <p>name: {{ person.name }}</p> <p>city: {{ person.address.city }}</p> <button @click="changeName">修改name</button> <button @click="changeCity">修改city</button> </div></template>

上面这段代码中:

我们在 changeCity 方法中直接更改 address 属性的值。可以看到,点击“修改city”按钮时,控制台会打印。

这里我们可以回想到,在 Vue2 中,侦听引用类型的数据时,需要深度侦听,Vue3也应该如此,所以我们给 watch 函数的第三个参数加个 deep: true 属性试试。

<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' // 使用 reactive 定义一个响应式对象 person let person = reactive({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changeCity const changeCity = () => { person.address.city = 'San Francisco' } // 定义一个方法 changeAddress const changeAddress = () => { person.address = { city: 'ShangHai' } } // 使用 watch 函数侦听 address 属性的变化 watch( () => person.address, (newValue, oldValue) => { console.log(`新:${JSON.stringify(newValue)}`) console.log(`旧:${JSON.stringify(oldValue)}`) }, { deep: true } )</script><template> <div id="home"> <p>city: {{ person.address.city }}</p> <button @click="changeCity">修改city</button> <button @click="changeAddress">修改address</button> </div></template>

上面代码中:

我们给 watch 函数的第三个参数加了 deep: true 属性。此时,我们不管是点击“修改city”按钮,还是点击“修改address”按钮,都会在控制台打印输出。

这里我们可以总结出一个结论:

当我们侦听的属性类型是复杂的数据类型时,需要修改属性本身,才能触发 watch 侦听,如果要修改更深层的属性,需要将 deep 设置为 true 才能进行深度监听。

侦听ref声明的响应式对象

首先,当侦听的属性类型是简单数据类型时:

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' // 使用 ref 定义一个响应式属性 count,初始值为 0 let count = ref(0) // 定义一个方法 changeCount,用于修改 count 值 const changeCount = () => { count.value++ } // 使用 watch 函数侦听 count 属性的变化 watch(count, (newValue, oldValue) => { console.log(`新count值:${newValue},`, `旧count值:${oldValue}`) })</script> <template> <div id="home"> <p>count: {{ count }}</p> <button @click="changeCount">修改Count</button> </div> </template>

上面这段代码中:

我们使用响应式 ref 定义了一个变量 count,其初始值为 0,然后定义了一个方法 changeCount,该方法用于按钮点击时更改 count 的值。然后使用 watch 函数侦听响应式 count 变量值的变化,当 count 变量的值发生变化时,watch 回调函数将被执行,并将新值和旧值作为参数传递给该函数。当我们点击按钮时,控制台会输出如下的内容。

上面的 watch 写法,也可以改成下面这种:

// 使用 watch 函数侦听 count 属性的变化watch( () => count.value, (newValue, oldValue) => { console.log(`新count值:${newValue},`, `旧count值:${oldValue}`) })

当侦听的数据类型是复杂数据类型时:

当我们使用 ref 声明复杂数据类型时,内部会使用 reactive 将数据转化为 Proxy 类型。

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' // 使用 ref 定义一个响应式对象 person let person = ref({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changePerson const changePerson = () => { person.value = { name: 'Steven', address: { city: 'San Francisco' } } } // 定义一个方法 changeCity const changeCity = () => { person.value.address.city = 'ShangHai' } // 使用 watch 函数侦听 address 属性的变化 watch(person, (newValue, oldValue) => { console.log(newValue, oldValue) })</script><template> <div id="home"> <p>city: {{ person.address.city }}</p> <button @click="changePerson">修改person</button> <button @click="changeCity">修改city</button> </div></template>

上面这段代码中:

我们使用 watch 函数侦听 person 属性的变化,当我们点击“修改person”按钮时,会触发 watch 函数,而点击“修改city”按钮时,不会触发。

下面,我们给 watch 函数的第三个参数加个 deep: true 属性。

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' // 使用 ref 定义一个响应式对象 person let person = ref({ name: 'Echo', address: { city: 'GuangZhou' } }) // 定义一个方法 changePerson const changePerson = () => { person.value = { name: 'Steven', address: { city: 'San Francisco' } } } // 定义一个方法 changeCity const changeCity = () => { person.value.address.city = 'ShangHai' } // 使用 watch 函数侦听 address 属性的变化 watch( person, (newValue, oldValue) => { console.log(newValue, oldValue) }, { deep: true } )</script><template> <div id="home"> <p>city: {{ person.address.city }}</p> <button @click="changePerson">修改person</button> <button @click="changeCity">修改city</button> </div></template>

此时,我们可以看到,不管是点击哪个按钮,都可以触发 watch 函数。

因此,我们也可以得出一个结论:

当我们侦听的属性类型是复杂的数据类型时,如果要修改更深层的属性,需要将 deep 设置为 true 才能进行深度监听。

侦听多个来源组成的数组

watch 还可以侦听数组,前提是这个数组内部含有响应式数据。

<script setup> // 引入 vue 提供的 ref、reactive 和 watch API import { ref, reactive, watch } from 'vue' let a = ref(0) let b = reactive({ num: 0 }) // 使用 watch 函数侦听多个来源的数组 watch( [a, () => b.num], ([newA, newB], [oldA, oldB]) => { console.log(`a的值新:${newA}`, `a的值旧:${oldA}`) console.log(`b的值新:${newB}`, `b的值旧:${oldB}`) } ) // 定义一个方法 changeA,修改 a 的值 const changeA = () => { a.value += 10 } // 定义一个方法 changeB,修改 b 的值 const changeB = () => { b.num = 20; }</script><template> <div id="home"> <p>a: {{ a }}</p> <p>b: {{ b.num }}</p> <button @click="changeA">修改A</button> <button @click="changeB">修改B</button> </div></template>

上面这段代码中:

我们定义了两个变量 a 和 b,其中 a 是通过 ref 定义的,b 是通过 reactive 定义的。然后使用 watch 侦听 a 和 b.num 的值,当 a 的值或者 b 的值发生变化时,都会触发 watch 函数。

深层侦听器

我们在前面的代码中有提到过,如果我们使用 watch 函数侦听一个响应式对象时,只要对象里面的某个属性发生了变化,那么就会执行侦听器回调函数。

原因是因为直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器。

但是,如果我们是使用的 getter 函数返回响应式对象的形式,如果不添加深层侦听器,那么响应式对象的属性值发生变化时,是不会触发 watch 的回调函数的。

<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' let count = reactive({ number: 0 }) const changeCountNumber = () => { count.number++ } watch( () => count, (newValue, oldValue) => { console.log(newValue, oldValue) } )</script><template> <div id="home"> <p>count的number值: {{ count.number }}</p> <button @click="changeCountNumber">修改number</button> </div></template>

上面这段代码中:

我们使用 reactive 定义了一个响应式对象 count,然后定义了一个方法 changeCountNumber,该方法主要用于改变响应式对象 count 中 number 的值。然后使用 watch 函数侦听响应式对象,其中数据源用 getter 函数返回了响应式对象,当我们更改 count 中 number 的值时,watch 的回调函数是不会执行的。

为了实现上述代码的侦听,我们可以手动给侦听器加上深层侦听的效果。

添加深层侦听很简单,我们只需要给 watch 函数添加第三个参数 { deep: true } 即可。

<script setup> // 引入 vue 提供的 reactive 和 watch API import { reactive, watch } from 'vue' let count = reactive({ number: 0 }) const changeCountNumber = () => { count.number++ } watch( () => count, (newValue, oldValue) => { console.log(newValue, oldValue) }, { deep: true } )</script><template> <div id="home"> <p>count的number值: {{ count.number }}</p> <button @click="changeCountNumber">修改number</button> </div></template>

上面这段代码中:

我们给 watch 添加深层侦听,当响应式对象 count 中的 number 值发生变化时,会触发 watch 函数。此时我们可以看到 newValue 和 oldValue 的值是相等的,除非我们把响应式对象即 count 整个替换掉,那么这两个值才会变得不一样。

需要注意的是:深度侦听需要遍历被侦听对象中的所有嵌套的属性,当用于大型数据结构时,开销很大。因此请只在必要时才使用它,并且要留意性能。

即时回调的侦听器

watch 默认是懒执行的:仅当数据源发生变化时,才会执行回调。但在某些场景中,我们希望在创建侦听器时,立即执行一遍回调。

我们只需要给 watch 函数添加第三个参数 { immediate: true } 即可。

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' let count = ref(0) const changeCount = () => { count.value++ } watch( count, (newValue, oldValue) => { console.log(`新的count:${newValue},旧的count:${oldValue}`) }, { immediate: true } )</script><template> <div id="home"> <p>count值: {{ count }}</p> <button @click="changeCount">修改count</button> </div></template>

上面这段代码中:

我们给 watch 函数添加第三个参数 { immediate: true }。在第一次进入页面时,会先调用一次 watch 回调函数,然后当 count 值发生变化时,会再次触发 watch 函数执行回调。

回调的触发时机

大家思考一个问题:如果我们在侦听器的回调函数中来获取 DOM,此时我们获取到的这个 DOM 是更新前的还是更新后的?

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' let name = ref('张三') const nameRef = ref(); const changeName = () => { name.value = '李四' } watch( name, (newValue, oldValue) => { console.log(`新的name:${newValue},旧的name:${oldValue}`) console.log(`DOM 节点:${nameRef.value.innerHTML}`) } )</script><template> <div id="home"> <p ref="nameRef">name: {{ name }}</p> <button @click="changeName">修改name</button> </div></template>

上面这段代码中:

我们通过点击按钮更改 name 的值,把“张三”修改成“李四”。但是我们发现在侦听器的回调函数里面获取到的 DOM 节点里面的内容还是“张三”,说明在侦听器回调中访问的 DOM 是 Vue 更新之前的状态。

如果想在侦听器回调中访问 Vue 更新之后的 DOM,我们只需要再给侦听器多传递一个参数选项即可:flush: 'post'。

<script setup> // 引入 vue 提供的 ref 和 watch API import { ref, watch } from 'vue' let name = ref('张三') const nameRef = ref(); const changeName = () => { name.value = '李四' } watch( name, (newValue, oldValue) => { console.log(`新的name:${newValue},旧的name:${oldValue}`) console.log(`DOM 节点:${nameRef.value.innerHTML}`) }, { flush: 'post' } )</script><template> <div id="home"> <p ref="nameRef">name: {{ name }}</p> <button @click="changeName">修改name</button> </div></template>

此时,我们可以看到,获取到的 DOM 就是 Vue 更新之后的 DOM 了。

作者:前端小码哥链接:https://juejin.cn/post/7293789260581634089

0 阅读:0

互联架构唠唠嗑

简介:感谢大家的关注