前提摘要:
本文是在基于
Vue3
的:v3.4.21
版本基础上进行整理的。后续官方如有版本更新有关watch
的新特性欢迎留言讨论。
watch
概述
官网对 watch 描述
侦听
一个或多个响应式
数据源,并在数据源变化时调用所给的回调函数。
详细信息
watch()
默认是懒侦听
的,即仅在侦听源发生变化时
才执行回调函数。
watch() 总共接收三个参数
第一个参数
是需要被监听的数据源
。这个来源必须是以下几种:
- 一个
getter 函数
,返回一个值 - 一个
ref()
定义的响应式数据 - 一个
reactive()
定义的响应式数据 - 以及由
以上类型的值组成的数组
官方文档描述
第二个参数
是在监听的数据源发生变化时要调用的
回调函数
。这个回调函数接受三个
参数:新值(newValue)、旧值(oldValue)
,以及一个用于注册副作用清理的回调函数
。该回调函数
会在副作用下一次重新执行前
调用,可以用来清除无效的副作用,例如等待中的异步请求。
示例
watch(data, (newValue, OldValue, onCleanup) => {
console.log("数据变化了", `新值是:${newValue}`, `旧值是${OldValue}`);
onCleanup(() => {
console.log("清除副作用");
});
});
当同时监听
多个来源时
,回调函数接受两个数组,分别对应来源数组中的新值和旧值
。
示例
watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => {
/* ... */
})
第三个参数是一个可选参数
(options 配置对象
),支持以下这些配置选项:
immediate
:在侦听器创建时立即触发回调
。第一次调用时旧值是undefined
deep
:如果源是对象
,强制深度遍历
,以便在深层级变更时触发回调。参考深层监听器flush
:调整回调函数的刷新时机。参考回调刷新的时机以及watchEffect()onTrack / onTrigger
:调试侦听器的依赖。参考侦听器调试once
:回调函数只会运行一次
。侦听器将在回调函数首次运行后自动停止
。【 3.4+ 版本特性 】
【配置项参数类型
】
interface WatchOptions extends WatchEffectOptions {
immediate?: boolean // 默认:false
deep?: boolean // 默认:false
flush?: 'pre' | 'post' | 'sync' // 默认:'pre'
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
once?: boolean // 默认:false (3.4+)
}
使用场景一:
监听
ref()
定义的【基本类型
】数据:watch 参数一
直接写数据名即可,监听的是其value
值的改变。
案例:
<template>
<h1>{{ numdata }}</h1>
<button @click="shownum">点击修改</button>
</template>
<script setup lang="ts" name="Box">
import { ref, watch } from 'vue';
type typeData = string | number | boolean | BigInt | Symbol | null | undefined;
let numdata = ref<typeData>(0);
const shownum = (): void => {
(numdata.value as number) += 1
};
watch(numdata, (newValue, OldValue): void => {
console.log("数据变化了", `新值是:${newValue}`, `旧值是${OldValue}`);
});
</script>
使用场景二:
监视
ref()
定义的【引用类型
】数据:直接写数据名,监视的是源数据
的【地址值(堆 / 栈 内存的关系
)】,若想深层监视对象内部的数据,则需要手动开启深度监视
。
案例:
<template>
<h1>{{ numdata.name }}</h1>
<h1>{{ numdata.age }}</h1>
<button @click="shownum">修改名</button>
<button @click="shownum2">修改年龄</button>
<button @click="shownum3">修改全部</button>
</template>
<script setup lang="ts" name="Box">
import { ref, watch } from 'vue';
type typeData = {
name: string,
age: number
};
let numdata = ref<typeData>({
name: "张三",
age: 1
});
let describe = ref<string>("");
const shownum = (): void => {
numdata.value.name = "李四";
describe.value = "名字被修改"
};
const shownum2 = (): void => {
numdata.value.age = 456;
describe.value = "年龄被修改"
};
const shownum3 = (): void => {
numdata.value = {
name: "王五",
age: 789
}
describe.value = "整体被修改"
};
watch(numdata, (newValue, OldValue): void => {
console.log("数据变化了",describe.value, newValue, OldValue);
});
</script>
在上方案例中:
依次点击三个事件的时候,会出现一个问题,那就是只有在触发整体被修改
事件,整体
更改numdata.value
属性值的时候,watch 数据监听
才会触发,而如果只更改了numdata
数据中的某一个属性值
,watch 数据监听
则不会触发
,这是因为,watch
在监听引用数据类型
的时候,是监听的当前数据的引用指向地址(堆内存)
的变化,而当只更改当前数据某一个属性值的时候,对于watch
来说,它的堆内存 地址
并没有改变,所以就认为数据没有发生变化
,但是当整个修改numdata.value
数据的时候,相当于重新分配了 一块堆内存地址
,这时候,对于watch
来说,数据才发生变化。所以才会监听触发。
针对上面的问题,我们需要在
watch
的第三个参数配置对象中 对监听的数据手动开启深度监视
{ deep: true }
(数组同理:如果直接通过数组下标索引
修改值,默认也是监听不到的,也需要 开启{ deep: true }
)
watch(numdata, (newValue, OldValue): void => {
console.log("数据变化了", describe.value, newValue, OldValue);
}, { deep: true }); // 配置 { deep: true } 开始深度监视
值得注意
在上面的情况中,还有一个问题值得注意:那就是当我们,在修改数据中某一个属性的时候 会出现
newValue
和oldValue
都是同一个新值
的情况,因为它们是指向同一个源对象地址
(数组同理:如果直接通过数组下标索引
修改某个值则newValue
和oldValue
也会是同一个新值
的情况)。 若修改了整个 ref.value
的值 ,newValue
就则是新的值,oldValue
则是旧的值,这是因为相当于从新分配了一块内存空间
,新旧数据已经不指向同一块源对象地址
了。
使用场景三:
监视
reactive()
定义的响应式数据时,且默认自动开启了深度监视
,并且该深度监视不可通过配置项 { deep: false } 关闭
。
官方文档描述
案例
<template>
<h1>{{ numdata.name }}</h1>
<h1>{{ numdata.age }}</h1>
<button @click="shownum">修改名</button>
<button @click="shownum2">修改年龄</button>
<button @click="shownum3">修改全部</button>
</template>
<script setup lang="ts" name="Box">
import { reactive, watch } from 'vue';
type typeData = {
name: string,
age: number
};
let numdata = reactive<typeData>({
name: "张三",
age: 1
});
const shownum = (): void => {
numdata.name = "李四";
console.log("名字被修改");
};
const shownum2 = (): void => {
numdata.age = 456;
console.log("年龄被修改");
};
const shownum3 = (): void => {
console.log("修改全部");
/****
* 此处 调用 Object.assign 合并整合新数据和旧数据,并不算真正意义上的修改掉
*
*
* **/
Object.assign(numdata, { name: "王五", age: 789 }) //
};
watch(numdata, (newValue, OldValue): void => {
console.log("数据变化了", newValue, OldValue);
// 注意:当只修改嵌套的属性触发监听时 `newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
}, { deep: false }); // { deep: false } 默认开启深度监视,且无法通过配置项关闭。
</script>
修改数组也是同理
<template>
<h1>{{ numdata[0] }}</h1>
<h1>{{ numdata[1] }}</h1>
<button @click="shownum">修改下标0</button>
<button @click="shownum2">修改下标1</button>
</template>
<script setup lang="ts" name="Box">
import { reactive, watch } from 'vue';
let numdata = reactive<number[]>([1, 2]);
const shownum = (): void => {
numdata[0] = 11
console.log("下标0 被修改");
};
const shownum2 = (): void => {
numdata[1] = 22
console.log("下标 1 被修改");
};
watch(numdata, (newValue, OldValue): void => {
console.log("数据变化了", newValue, OldValue);
// 注意:当只修改嵌套的属性触发监听时 `newValue` 此处和 `oldValue` 是相等的
// 因为它们是同一个对象!
}, { deep: false }); // { deep: false } 默认开启深度监视,且无法通过配置项关闭。
</script>
【注意:
】
介于
reactive
的设计原理,在修改reactive
定义的数据时,无法整体修改其变量名数据,这会使其丢失响应式,故而,在操作reactive
定义的数据时,需保持只通过属性 key 或者 下标索引值
去修改某个数据,尽量避免直接修改reactive
数据源本体。
使用场景四:
监视
ref()
或reactive()
所定义的【引用数据
类型】数据中的某个属性 或 下标值
注意事项
- 若监听的该属性值为
基本数据类型
,则需要写成函数形式
你
不能直接侦听响应式对象的属性值,例如:
const obj = reactive({ count: 0 })
// 错误,因为 watch() 得到的参数是一个 number
watch(obj.count, (count) => {
console.log(`count is: ${count}`)
})
这里需要用一个返回该属性的 getter 函数
:
// 提供一个 getter 函数
watch(
() => obj.count,
(count) => {
console.log(`count is: ${count}`)
}
)
- 若该属性值是依然是【
引用数据
类型】,可直接编写所要监听属性(但会存在一些问题),也可写成函数,更加强烈建议写成函数。
案例
<template>
<h1>{{ numdata.name }}</h1>
<button @click="shownum">修改名</button>
<button @click="shownum2">修改全部</button>
</template>
<script setup lang="ts" name="Box">
import { ref, watch } from 'vue';
type typeData = {
name: string,
config: {
n1: number,
n2: number
}
};
let numdata = ref<typeData>({ //多级嵌套数据
name: "张三",
config: {
n1: 123,
n2: 456
}
});
const shownum = (): void => { //只修改名字
numdata.value.name = "李四";
console.log("名字被修改");
};
const shownum2 = (): void => { //修改整个 config 属性
console.log("修改全部");
numdata.value.config = { n1: 789, n2: 666 }
};
// 只监听 name 属性 当监视响应式对象中的某个属性,且该属性是基本数据类型的,必须要写成函数式
watch(() => numdata.value.name, (newValue, OldValue): void => {
console.log("数据变化了", newValue, OldValue);
});
/**
只监听整个 config 属性。 当监视响应式对象中的某个属性,且该属性依然是引用数据类型的,
可以直接写(不推荐),也能写函数(推荐),更加推荐写成函数式,函数监视效果更加的高效。
**/
watch(() => numdata.value.config, (newValue, OldValue): void => {
console.log("数据变化了", newValue, OldValue);
}, { deep: true }); // 再根据业务,选择性开启 深度监视,监视监听源里面更加深层次的数据
</script>
使用场景五:
一次性监听多个
多个数据
写法格式为一个数组
<template>
<h1>{{ data1 }}</h1>
<h1>{{ data2.name }}</h1>
<button @click="shownum">修改data1</button>
<button @click="shownum2">修改data2</button>
</template>
<script setup lang="ts" name="Box">
import { ref, watch, reactive } from 'vue';
let data1 = ref<number>(1);
let data2 = reactive({
name: "张三"
});
const shownum = (): void => {
data1.value += 1
};
const shownum2 = (): void => {
data2.name = "李四"
};
// 同时监听 data1 和 data2.name 等多个数据的变化 写法格式为一个数组 ,回调函数接收两个数组,分别对应来源数组中的新值和旧值:
watch([data1, () => data2.name], ([newValuedata1, newValuedata2], [OldValuedata1, OldValuedata2]): void => {
console.log("数据变化了新值", newValuedata1, newValuedata2);
console.log("数据变化了旧值", OldValuedata1, OldValuedata2);
});
</script>
配置参数说明
deep
:如果源是对象
,强制深度遍历
,以便在深层级数据发生变更时触发回调
immediate
once
flush
如果想在侦听器回调中能访问被 Vue 更新之后
的所属组件的 DOM,你需要指明 flush: 'post'
选项:
示例
watch(source, callback, {
flush: 'post'
})
你还可以创建一个同步触发
的侦听器,它会在 Vue 进行任何更新之前
触发:
示例
watch(source, callback, {
flush: 'sync'
})
onTrack / onTrigger
侦听器调试追踪回调函数
示例
watch(source, callback, {
onTrack(e) {
// 当 source 被追踪为依赖时触发
debugger
},
onTrigger(e) {
// 当 source 被更改时触发
debugger
}
})
watchEffect()
立即运行一个函数,同时
响应式地追踪其依赖
,并在依赖更改时重新执行
。
详细信息
watchEffect()
的第一个参数
是一个要运行的回调函数。这个回调函数的参数也是一个函数
,用来注册清理回调。清理回调会在该副作用下一次执行前被调用,可以用来清理无效的副作用,例如等待中的异步请求
示例
watchEffect((onCleanup) => {
onCleanup(() => {
console.log("清除副作用");
})
})
watchEffect()
第二个参数
是一个可选的配置对象,可以用来调整副作用的刷新时机或调试副作用的依赖
支持配置项:flush
,onTrack
,onTrigger
示例
watchEffect((onCleanup) => {
onCleanup(() => {
console.log("清除副作用");
})
}, {
flush: 'post', // sync \ pre
onTrack(e) {
//被追踪为依赖时触发
console.log(e);
},
onTrigger(e) {
// 所追踪数据更改时触发
console.log(e);
}
})
watch 对比 watchEffect 的区别
watch
和watchEffect
都能响应式地执行有副作用的回调。它们之间的主要区别是追踪响应式依赖的方式
:
watch
只追踪明确侦听的数据源
。它不会追踪任何在回调中访问到的东西。另外,仅在数据源确实改变时才会触发回调。watch 会避免在发生副作用时追踪依赖,因此,我们能更加精确地控制回调函数的触发时机。同时还有以下几点:- 懒执行副作用;
- 更加明确是应该由哪个状态触发侦听器重新执行;
- 可以访问所侦听状态的
前一个值和当前值
watchEffect
,初始化时会自动执行一次并响应式的追踪其依赖。它会在同步执行过程中,自动追踪所有能访问到的响应式属性。这更方便,而且代码往往更简洁,但有时其响应性依赖关系会不那么明确
停止侦听器
示例
<script setup>
import { watchEffect } from 'vue'
// 它会自动停止
watchEffect(() => {})
// ...这个则不会!
setTimeout(() => {
watchEffect(() => {})
}, 100)
</script>
要
手动停止一个侦听器
,请调用watch
或watchEffect
返回的函数:
示例
const unwatch = watchEffect(() => {})
// ...当该侦听器不再需要时
unwatch();
let showWatch = watch(data, (newValue, OldValue, onCleanup) => {
console.log("数据变化了", `新值是:${newValue}`, `旧值是${OldValue}`);
onCleanup(() => {
console.log("清除副作用");
});
});
// ...当该侦听器不再需要时
showWatch()
🚵♂️ 博主座右铭:向阳而生,我还在路上!
——————————————————————————————
🚴博主想说:将持续性为社区输出自己的资源,同时也见证自己的进步!
——————————————————————————————
🤼♂️ 如果都看到这了,博主希望留下你的足迹!【📂收藏!👍点赞!✍️评论!】
——————————————————————————————