前言
在 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 中,当响应式状态发生变化时,都会触发一个回调函数。
下面我们一起来看一个简单的例子:
typescript
<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和计算属性
typescript
<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函数
typescript
<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声明的响应式对象
typescript
<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 对象中的某个属性时,又会是什么情况?
首先,当侦听的属性是简单数据类型时:
typescript
<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 方法。
当侦听的属性为复杂数据类型时:
typescript
<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 呢?
typescript
<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 属性试试。
typescript
<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声明的响应式对象
首先,当侦听的属性类型是简单数据类型时:
typescript
<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 写法,也可以改成下面这种:
typescript
// 使用 watch 函数侦听 count 属性的变化
watch(
() => count.value,
(newValue, oldValue) => {
console.log(`新count值:${newValue},`, `旧count值:${oldValue}`)
}
)
当侦听的数据类型是复杂数据类型时:
当我们使用 ref 声明复杂数据类型时,内部会使用 reactive 将数据转化为 Proxy 类型。
typescript
<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 属性。
typescript
<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 还可以侦听数组,前提是这个数组内部含有响应式数据。
typescript
<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 的回调函数的。
typescript
<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 } 即可。
typescript
<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 } 即可。
typescript
<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 是更新前的还是更新后的?
typescript
<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'。
typescript
<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 了。
总结
以上就是我对 Vue3.0 中 watch 函数的了解,希望可以帮助正在学习 Vue3.0 的朋友!
如果有不正确的地方,欢迎大家在评论区多多指正!
看完记得点个赞哦~ 谢谢!🤞🤞🤞