带你吃透 Vue3 中 侦听器 【watch ,watchEffect】数据监听的使用及注意事项

在这里插入图片描述

在这里插入图片描述

前提摘要:

本文是在基于 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 } 开始深度监视

值得注意

在上面的情况中,还有一个问题值得注意:那就是当我们,在修改数据中某一个属性的时候 会出现 newValueoldValue 都是同一个新值的情况,因为它们是指向同一个源对象地址(数组同理:如果直接通过 数组下标索引 修改某个值则 newValueoldValue 也会是同一个新值的情况)。 若修改了整个 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() 第二个参数是一个可选的配置对象,可以用来调整副作用的刷新时机或调试副作用的依赖

支持配置项:flushonTrackonTrigger
示例

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()


点击查看官方文档

在这里插入图片描述


🚵‍♂️ 博主座右铭:向阳而生,我还在路上!
——————————————————————————————
🚴博主想说:将持续性为社区输出自己的资源,同时也见证自己的进步!
——————————————————————————————
🤼‍♂️ 如果都看到这了,博主希望留下你的足迹!【📂收藏!👍点赞!✍️评论!】
——————————————————————————————

  • 13
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

旧梦星轨

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值