Open beichensky opened 2 years ago
在使用 Vue3 提供的 watch API 时
Vue3
watch API
有时会遇到监听的数据变了,但是不触发 watch 的情况;
watch
有时修改数据会触发 watch,重新赋值无法触发;
有时重新赋值能触发 watch,但是修改内部数据又不触发;
再或者监听外部传入的数据时,是否和直接监听组件内部数据时的行为一致?
面临这些问题,决心通过下面的应用场景一探究竟!避免重复踩坑,对应不同的问题,找到合适的解决方案。
本文已收录在 Github: github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
使用 reactive 声明的响应式数据,类型是 Proxy
reactive
Proxy
使用 ref 声明的响应式数据,类型是 RefImpl
ref
RefImpl
使用 computed 得到的响应式数据,类型也属于 RefImpl
computed
使用 ref声明时,如果是引用类型,内部会将数据使用 reactive 包裹成 Proxy
watch(source, callback, options)
source: 需要监听的响应式数据或者函数
source
callback:监听的数据发生变化时,会触发 callback
callback
newValue:数据的新值
newValue
oldValue:数据的旧值
oldValue
onCleanup:函数类型,接受一个回调函数。每次更新时,会调用上一次注册的 onCleanup 函数
onCleanup
options:额外的配置项
options
immediate:Boolean类型,是否在第一次就触发 watch
immediate
Boolean
deep:Boolean 类型,是否开启深度监听
deep
flush:pre | post | sync
flush
pre
post
sync
pre:在组件更新前执行副作用
post:在组件更新后运行副作用
sync:每个更改都强制触发 watch
onTrack:函数,具备 event 参数,调试用。将在响应式 property 或 ref 作为依赖项被追踪时被调用
onTrack
event
property
onTrigger:函数,具备 event 参数,调试用。将在依赖项变更导致副作用被触发时被调用。
onTrigger
当监听的 reactive 声明的响应式数据时,修改响应式数据的任何属性,都会触发 watch
const state = reactive({ name: '张三', address: { city: { cityName: '上海', }, }, }); watch( state, (newValue, oldValue) => { console.log(newValue, oldValue); }, { deep: false, } ); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '北京'; }, 2000);
可以发现,name 和 cityName 发生变化时,都会触发 watch。但是,这里会发现两个问题:
name
cityName
无论是修改 name 或者 cityName 时,oldValue 和 newValue 的值是一样的;
尽管我们将 deep 属性设置成了 false,但是 cityName 的变化依然会触发 watch。
false
当监听的响应式数据是 Proxy 类型时,newValue 和 oldValue 由于是同一个引用,所以属性值是一样的;
当监听的响应式数据是 Proxy 类型时,deep 属性无效,无论设置成 true 还是 false,都会进行深度监听。
true
由于在业务开发中,定义的数据中可能属性比较多,我们指向监听其中某一个属性,那我们看看该如何操作
如果只想监听 name 属性时,由于 name 是个基本类型,所以 source 参数需要用回调函数的方式进行监听:
watch( () => state.name, (newValue, oldValue) => { console.log(newValue, oldValue); } ); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '北京'; }, 2000);
这是可以看到,newValue 为 张三,oldValue 为 李四,并且在修改 cityName 时,不会再触发 watch。
监听 address 属性时,我们也可以使用回调函数的方式进行监听
address
watch( () => state.address, (newValue, oldValue) => { console.log(newValue, oldValue); } ); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '北京'; }, 2000);
豁。。。发现控制台现在一次日志 都不打印了,按道理说,修改 name 时,不触发 watch是正常的,但是修改 cityName 时,是想要触发的啊。
先看一下现在这种情况,如何触发 watch:
watch( () => state.address, (newValue, oldValue) => { console.log(newValue, oldValue); } ); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address = { city: { cityName: '北京', }, }; }, 2000);
这个时候,发现 1秒 和 2秒 之后,控制台出现打印结果了。那我们知道了,需要修改 address 属性,才能触发监听,修改更深层的属性,触发不了,这个时候明白了,应该是没有深度监听,Ok,那我们把 deep 属性设置为 true 试试:
watch( () => state.address, (newValue, oldValue) => { console.log(newValue, oldValue); }, { deep: true, } ); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '杭州'; }, 3000); setTimeout(() => { state.address = { city: { cityName: '北京', }, }; }, 2000);
果不其然,控制台中,正常的打印了两次日志,说明,无论直接修改 address 还是修改 address 内部的深层属性,都可以正常的触发 watch。
好的,到这里,可能有些同学说了:那我直接监听 state.address 不就可以了吗?这样 deep 属性也不用加。
state.address
那我们演示一下看看会不会存在问题:
watch(state.address, (newValue, oldValue) => { console.log(newValue, oldValue); }); setTimeout(() => { state.address.city.cityName = '杭州'; }, 3000); setTimeout(() => { state.address = { city: { cityName: '北京', }, }; }, 2000);
在控制台,只打印了第一次修改 cityName 时的日志,第二次修改 address 时,无法触发 watch
好,现在把上面两次修改调换一下位置:
watch(state.address, (newValue, oldValue) => { console.log(newValue, oldValue); }); setTimeout(() => { state.address = { city: { cityName: '北京', }, }; }, 2000); setTimeout(() => { state.address.city.cityName = '杭州'; }, 3000);
控制台里,一次日志都没有了,也就意味着,修改 address 时,无法触发监听,并且之后,由于 address 的引用发生变化,导致后续 address 内部的任何修改也都触发不了 watch 了。这是一个致命问题。
当指向监听响应式数据的某一个属性时,需要使用函数的方式设置 source 参数:
如果属性类型是基本类型,可以正常监听,并且 newValue 和 oldValue ,可以正常返回;
如果属性类型是引用类型,需要将 deep 设置为 true 才能进行深度监听。
如果属性类型时引用类型,并且没有用函数的方式注册 watch,那么在使用时,一旦重新对该属性赋值,会导致监听失效。
ref 声明的数据为基本类型时,直接使用 watch 监听即可
const state = ref('张三'); watch(state, (newValue, oldValue) => { console.log(newValue, oldValue); }); setTimeout(() => { state.value = '李四'; }, 1000);
1秒 后,在控制台可以看到,打印出了 李四 和 张三。
众所周知,ref 声明的数据,都会自带 value 属性。所以下面这种写法效果同上:
value
const state = ref('张三'); watch(() => state.value, (newValue, oldValue) => { console.log(newValue, oldValue); }); setTimeout(() => { state.value = '李四'; }, 1000);
ref 声明的数据为引用类型时,内部会接入 reactive 将数据转化为 Proxy 类型。所以该数据的 value 对应的是 Proxy 类型。
const state = ref({ name: '张三', address: { city: { cityName: '上海', }, }, }); watch(state, (newValue, oldValue) => { console.log(newValue, oldValue); }); setTimeout(() => { state.value = { name: '李四', address: { city: { cityName: '上海', }, }, }; }, 1000); setTimeout(() => { state.value.address.city.cityName = '北京'; }, 2000);
1秒后,控制台打印出了日志,但是 2秒后,却没有日志再出现了,这又是什么原因呢,我们把上面的代码转个形。在 ref 声明的数据为基本类型时,这段里说过,监听 state 和 () => state.value ,效果是一样的,那我们看一下转换后的代码:
state
() => state.value
watch(() => state.value, (newValue, oldValue) => { console.log(newValue, oldValue); });
上面说了,当 ref 声明的数据是引用类型时,内部会借助 reactive 转化为 Proxy 类型。那这段代码是不是感觉似曾相识?哈哈,不就是将 deep 属性设置为 true 就可以了么。
const state = ref({ name: '张三', address: { city: { cityName: '上海', }, }, }); watch( state, (newValue, oldValue) => { console.log(newValue, oldValue); }, { deep: true, } ); setTimeout(() => { state.value = { name: '李四', address: { city: { cityName: '上海', }, }, }; }, 1000); setTimeout(() => { state.value.address.city.cityName = '北京'; }, 2000);
加上 deep 之后,可以看到,在 1秒及 2秒后,都会在控制台打印出日志。说明此时,无论是修改 state 的 value,还是修改深层属性,都会触发 watch。
有些同学可能说了,我直接函数返回 state 行不行:
const state = ref({ name: '张三', address: { city: { cityName: '上海', }, }, }); watch( () => state, (newValue, oldValue) => { console.log(newValue, oldValue); } ); setTimeout(() => { state.value = { name: '李四', address: { city: { cityName: '上海', }, }, }; }, 1000); setTimeout(() => { state.value.address.city.cityName = '北京'; }, 2000);
好的,这里我帮大家试过了,跟上面的效果有些区别:
当 deep 为 false 时,修改 value 或者深层属性,都不会触发 watch
而设置deep 为 true 时,修改 vaue 或者深层属性,都会触发 watch
既然是 Proxy 类型的数据,那么我们直接按照之前演示的方式,直接使用不就好了么:
App 组件
<script setup> import { reactive } from 'vue'; import Child from './Child.vue'; const state = reactive({ name: '张三', address: { city: { cityName: '上海', }, }, }); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '北京'; }, 2000); </script> <template> <Child :data="state" /> </template>
Child 组件
<script setup> import { watch } from 'vue'; const props = defineProps(['data']); watch(props.data, (newValue, oldValue) => { console.log(newValue, oldValue); }); </script>
好的,在 1秒 和 2秒之后,可以看到控制台打印出的有两次日志。Ok,乍一看感觉没有问题哈,那我们修改一下 App 组件里的数据传递:
App
<script setup> import { reactive, ref } from 'vue'; import Child from './Child.vue'; const state = reactive({ name: '张三', address: { city: { cityName: '上海', }, }, }); const otherState = reactive({ name: '李四', }); const flag = ref(true); setTimeout(() => { flag.value = false; }, 500); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '北京'; }, 2000); </script> <template> <Child :data="flag ? otherState : state" /> </template>
有些同学可能就问,flag ? otherState : state 这里用 computed 包装一下不行吗?当然可以,但是这里不是为了演示问题嘛,一切写法皆有可能对吧。
flag ? otherState : state
修改完 App 组件之后,按道理应该会打印三次日志,但是惊讶的发现:无论多久,控制台里都不会有日志打印,也就是说,data 属性的变化根本没有触发 watch。这是为啥呢?又该怎么处理呢?
因为在 App 组件中,我们切换了要传递给 Child 组件的数据,所以 watch 监听的 prop 不是同一个了
Child
prop
所以需要使用函数的方式监听 prop
<script setup> import { watch } from 'vue'; const props = defineProps(['data']); watch(() => props.data, (newValue, oldValue) => { console.log(newValue, oldValue); }); </script>
确实哈,修改完之后,控制台里打印了一次日志,而且新旧值不同,说明切换数据的时候监听到了,但还是不对,还少了两次。
到了这里,还记得我们上面讨论过的,使用函数作为 source 监听时,想监听深层的属性,那就需要添加 deep 属性为 true 才可以。
<script setup> import { watch } from 'vue'; const props = defineProps(['data']); watch( () => props.data, (newValue, oldValue) => { console.log(newValue, oldValue); }, { deep: true, } ); </script>
好的,添加了 deep: true 之后,控制台中分别在 500ms、1秒、2秒后打印出了日志。此时达到了想要的效果。很棒!
deep: true
当 ref 定义的数据作为 prop 进行传递时,会进行脱 ref 的操作,也就是说,基本类型会直接将数据作为 prop 传递,引用类型会作为 Proxy 传入
直接使用函数作为 source 参数,进行监听即可:
<script setup> import { ref } from 'vue'; import Child from './Child.vue'; const state = ref('张三'); setTimeout(() => { state.value = '李四'; }, 1000); </script> <template> <Child :data="state" /> </template>
Child 组件,此时由于 ref 定义的是基本数据类型,所以也不存在是否需要深度监听的问题
<script setup> import { watch } from 'vue'; const props = defineProps(['data']); watch( () => props.data, (newValue, oldValue) => { console.log(newValue, oldValue); } ); </script>
上面说过,ref 作为 prop 传递时,会脱 ref,也就意味着,传给子组件的就是 Proxy 类型的数据,用法及可能遇到的问题,请参照 proxy 作为 prop 传递时 里的代码和示例。
同proxy 作为 prop 传递时,请参照 proxy 作为 prop 传递时 里的代码和示例。
provide API 提供的数据为 ref 时,不会进行脱 ref 操作,同 四、watch 监听 ref 声明的响应式数据,请参照 四、watch 监听 ref 声明的响应式数据 里的代码和示例
provide API
可以在 watch 中使用函数的方式进行监听,前提是需要将 deep 设置 true 哦,这样对象内部如果包含了响应式的数据,也是可以触发监听的。
在实际开发过程中,可能会需要同时监听多个值,我们看一下多个值的情况,watch 是如何处理以及响应的:
import { reactive, ref, watch } from 'vue'; const state = reactive({ name: '张三', address: { city: { cityName: '上海', }, }, }); consotherState = reactive({ name: '李四', }); const flag = ref(true); watch([state, () => otherState.name, flag], (newValue, oldValue) => { console.log(newValue, oldValue); }); setTimeout(() => { flag.value = false; }, 500); setTimeout(() => { otherState.name = '李四'; }, 1000); setTimeout(() => { state.address.city.cityName = '北京'; }, 2000);
可以再控制台看到,三次变化都会输出日志,并且 newValue 和 oldValue 都是一个数组,里面值的顺序对应着 source 里数组的顺序。
在业务开发的过程中,时常面临这样的需求:监听某个数据的变化,当数据发生变化时,重新进行网络请求。下面写一段代码,来模拟这个需求:
<script setup> import { reactive, ref, watch } from 'vue'; let count = 2; const loadData = (data) => new Promise((resolve) => { count--; setTimeout(() => { resolve(`返回的数据为${data}`); }, count * 1000); }); const state = reactive({ name: '张三', }); const data = ref(''); watch( () => state.name, (newValue) => { loadData(newValue).then((res) => { data.value = res; }); } ); setTimeout(() => { state.name = '李四'; }, 100); setTimeout(() => { state.name = '王五'; }, 200); </script> <template> <div>{{ data }}</div> </template>
可以看到界面上展示的结果是:返回的数据为李四,显然这不是我们想要的结果。最后一次是将 name 修改为了王五,所以肯定是希望返回的结果为王五。那出现这个异常的原因是什么呢?
数据每次变化,都会发送网络请求,但是时间长短不确定,所以就有可能导致,后发的请求先回来了,所以会被先发的请求返回结果给覆盖掉。
那么该如何解决呢?上面提到过,watch 的 callback 中具备第三个参数 onCleanup,我们来尝试着用一下:
watch( () => state.name, (newValue, oldValue, onCleanup) => { let isCurrent = true; onCleanup(() => { isCurrent = false; }); loadData(newValue).then((res) => { if (isCurrent) { data.value = res; } }); } );
此时,在浏览器上,只会出现:返回的数据为王五。
onCleanup 接受一个回调函数,这个回调函数,在触发下一次 watch 之前会执行,因此,可以在这里,取消上一次的网络请求,亦或做一些内存清理及数据变更等任何操作。
上面说了很多 watch 的应用场景和常见问题。在需要监听多个数据时,可以使用数组作为 source。但是多个数据,如果是很多个呢,可能比较负责的逻辑,其中使用了较多的响应式数据,这个时候,使用 watch 去监听,显然不太适合。
这里可以使用新的 API:watchEffect
watchEffect(effect, options)
effect: 函数。内部依赖的响应式数据发生变化时,会触发 effect 重新执行
effect
options:
pre:在组件更新前执行副作用;
post:在组件更新后运行副作用,可以使用 watchPostEffect 替代;
watchPostEffect
sync:每个更改都强制触发 watch,可以使用 watchSyncEffect 替代。
watchSyncEffect
import { reactive, ref, watchEffect } from 'vue'; const state = reactive({ name: '张三', }); const visible = ref(false); watchEffect(() => { console.log(state.name, visible.value); }); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { visible.value = true; }, 2000);
2秒之后,查看控制台,发现打印了三次日志:
第一次是初始值
第二次是修改 name 触发的监听
第三次是修改 visible 触发的监听
而且每次打印的都是当前最新值
由此可以看出:
watchEffect 默认监听,也就是默认第一次就会执行;
不需要设置监听的数据,在 effect 函数中,用到了哪个数据,会自动进行依赖,因此不用担心类似 watch 中出现深层属性监听不到的问题;
只能获取到新值,由于没有提前指定监听的是哪个数据,所以不会提供旧值。
在上面的用法中,感觉 watchEffect 使用起来还是很方便的,会自动依赖,而且还不用考虑各种深度依赖的问题。那 watchEffect 会不会有什么陷阱需要注意呢?
watchEffect
import { reactive, ref, watchEffect } from 'vue'; const state = reactive({ name: '张三', }); const visible = ref(false); watchEffect(() => { setTimeout(() => { console.log(state.name, visible.value); }) }); setTimeout(() => { state.name = '李四'; }, 1000); setTimeout(() => { visible.value = true; }, 2000);
这次在 effect 函数中添加了异步任务,在 setTimeout 中使用响应式数据,会发现,控制台一直都只展示一个日志:第一次进入时打印的。
setTimeout
也就是说,在异步任务(无论是宏任务还是微任务)中进行的响应式操作,watchEffect 无法正确的进行依赖收集。所以后面无论数据如何变更,都不会触发 effect 函数。
如果真的需要用到异步的操作,可以在外面先取值,再放到异步中去使用
watchEffect(() => { const name = state.name; const value = visible.value; setTimeout(() => { console.log(name, value); }); });
修改之后,在控制台中可以正常的看到三次日志。
当 wacth 的 source 为 Proxy 类型时:
wacth
deep 属性失效,强制进行深度监听;
新旧值指向同一个引用,导致内容是一样的。
当 watch 的 source 是 RefImpl 类型时:
直接监听 state 和 监听 () => state.value 是等效的;
如果 ref 定义的是引用类型,并且想要进行深度监听,需要将 deep 设置为 true。
当 watch 的 source 是函数时,可以监听到函数返回值的变更。如果想监听到函数返回值深层属性的变化,需要将 deep 设置为 true;
如果想监听多个值的变化,可以将 source 设置为数组,内部可以是 Proxy 对象,可以是 RefImpl 对象,也可以是具有返回值的函数;
在监听组件 props 时,建议使用函数的方式进行 watch,并且希望该 prop 深层任何属性的变化都能触发,可以将 deep 属性设置为 true;
props
使用 watchEffect 时,注意在异步任务中使用响应式数据的情况,可能会导致无法正确进行依赖收集。如果确实需要异步操作,可以在异步任务外先获取响应式数据,再将值放到异步任务里进行操作。
watch监视ref定义的数据
watch监视reactive定义的数据
watch 时 value 的问题
watchEffect 函数
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。
前言
在使用
Vue3
提供的watch API
时有时会遇到监听的数据变了,但是不触发
watch
的情况;有时修改数据会触发
watch
,重新赋值无法触发;有时重新赋值能触发
watch
,但是修改内部数据又不触发;再或者监听外部传入的数据时,是否和直接监听组件内部数据时的行为一致?
面临这些问题,决心通过下面的应用场景一探究竟!避免重复踩坑,对应不同的问题,找到合适的解决方案。
本文已收录在 Github: github.com/beichensky/Blog 中,欢迎 Star,欢迎 Follow!
一、Vue3 中响应式数据的两种类型
使用
reactive
声明的响应式数据,类型是Proxy
使用
ref
声明的响应式数据,类型是RefImpl
使用
computed
得到的响应式数据,类型也属于RefImpl
使用
ref
声明时,如果是引用类型,内部会将数据使用reactive
包裹成Proxy
二、Watch API
watch(source, callback, options)
source
: 需要监听的响应式数据或者函数callback
:监听的数据发生变化时,会触发callback
newValue
:数据的新值oldValue
:数据的旧值onCleanup
:函数类型,接受一个回调函数。每次更新时,会调用上一次注册的onCleanup
函数options
:额外的配置项immediate
:Boolean
类型,是否在第一次就触发watch
deep
:Boolean
类型,是否开启深度监听flush
:pre
|post
|sync
pre
:在组件更新前执行副作用post
:在组件更新后运行副作用sync
:每个更改都强制触发watch
onTrack
:函数,具备event
参数,调试用。将在响应式property
或ref
作为依赖项被追踪时被调用onTrigger
:函数,具备event
参数,调试用。将在依赖项变更导致副作用被触发时被调用。三、watch 监听 reactive 声明的响应式数据
1. 监听 reactive 声明的响应式数据时
当监听的
reactive
声明的响应式数据时,修改响应式数据的任何属性,都会触发watch
可以发现,
name
和cityName
发生变化时,都会触发watch
。但是,这里会发现两个问题:无论是修改
name
或者cityName
时,oldValue
和newValue
的值是一样的;尽管我们将
deep
属性设置成了false
,但是cityName
的变化依然会触发watch
。这里得出两个结论:
当监听的响应式数据是
Proxy
类型时,newValue
和oldValue
由于是同一个引用,所以属性值是一样的;当监听的响应式数据是
Proxy
类型时,deep
属性无效,无论设置成true
还是false
,都会进行深度监听。2. 监听 Proxy 数据中的某个属性时
当监听的属性是基本类型时
如果只想监听
name
属性时,由于name
是个基本类型,所以source
参数需要用回调函数的方式进行监听:当监听的属性为引用类型时
监听
address
属性时,我们也可以使用回调函数的方式进行监听豁。。。发现控制台现在一次日志 都不打印了,按道理说,修改
name
时,不触发watch
是正常的,但是修改cityName
时,是想要触发的啊。先看一下现在这种情况,如何触发
watch
:这个时候,发现 1秒 和 2秒 之后,控制台出现打印结果了。那我们知道了,需要修改
address
属性,才能触发监听,修改更深层的属性,触发不了,这个时候明白了,应该是没有深度监听,Ok,那我们把deep
属性设置为true
试试:果不其然,控制台中,正常的打印了两次日志,说明,无论直接修改
address
还是修改address
内部的深层属性,都可以正常的触发watch
。好的,到这里,可能有些同学说了:那我直接监听
state.address
不就可以了吗?这样deep
属性也不用加。那我们演示一下看看会不会存在问题:
在控制台,只打印了第一次修改
cityName
时的日志,第二次修改address
时,无法触发watch
好,现在把上面两次修改调换一下位置:
控制台里,一次日志都没有了,也就意味着,修改
address
时,无法触发监听,并且之后,由于address
的引用发生变化,导致后续address
内部的任何修改也都触发不了watch
了。这是一个致命问题。这里也得出了两个结论:
当指向监听响应式数据的某一个属性时,需要使用函数的方式设置
source
参数:如果属性类型是基本类型,可以正常监听,并且
newValue
和oldValue
,可以正常返回;如果属性类型是引用类型,需要将
deep
设置为true
才能进行深度监听。如果属性类型时引用类型,并且没有用函数的方式注册
watch
,那么在使用时,一旦重新对该属性赋值,会导致监听失效。四、watch 监听 ref 声明的响应式数据
1. ref 声明的数据为基本类型时
ref
声明的数据为基本类型时,直接使用watch
监听即可1秒 后,在控制台可以看到,打印出了 李四 和 张三。
众所周知,
ref
声明的数据,都会自带value
属性。所以下面这种写法效果同上:2. ref 声明的数据为引用类型时
ref
声明的数据为引用类型时,内部会接入reactive
将数据转化为Proxy
类型。所以该数据的value
对应的是Proxy
类型。1秒后,控制台打印出了日志,但是 2秒后,却没有日志再出现了,这又是什么原因呢,我们把上面的代码转个形。在
ref
声明的数据为基本类型时,这段里说过,监听state
和() => state.value
,效果是一样的,那我们看一下转换后的代码:上面说了,当
ref
声明的数据是引用类型时,内部会借助reactive
转化为Proxy
类型。那这段代码是不是感觉似曾相识?哈哈,不就是将deep
属性设置为true
就可以了么。加上
deep
之后,可以看到,在 1秒及 2秒后,都会在控制台打印出日志。说明此时,无论是修改state
的value
,还是修改深层属性,都会触发watch
。有些同学可能说了,我直接函数返回
state
行不行:好的,这里我帮大家试过了,跟上面的效果有些区别:
当
deep
为false
时,修改value
或者深层属性,都不会触发 watch而设置
deep
为true
时,修改 vaue 或者深层属性,都会触发 watch五、watch 监听传入的 prop 时
1. Proxy 作为 prop 传递时
既然是
Proxy
类型的数据,那么我们直接按照之前演示的方式,直接使用不就好了么:App 组件
Child 组件
好的,在 1秒 和 2秒之后,可以看到控制台打印出的有两次日志。Ok,乍一看感觉没有问题哈,那我们修改一下
App
组件里的数据传递:App 组件
修改完
App
组件之后,按道理应该会打印三次日志,但是惊讶的发现:无论多久,控制台里都不会有日志打印,也就是说,data 属性的变化根本没有触发watch
。这是为啥呢?又该怎么处理呢?因为在
App
组件中,我们切换了要传递给Child
组件的数据,所以watch
监听的prop
不是同一个了所以需要使用函数的方式监听
prop
Child 组件
确实哈,修改完之后,控制台里打印了一次日志,而且新旧值不同,说明切换数据的时候监听到了,但还是不对,还少了两次。
到了这里,还记得我们上面讨论过的,使用函数作为
source
监听时,想监听深层的属性,那就需要添加deep
属性为true
才可以。Child 组件
好的,添加了
deep: true
之后,控制台中分别在 500ms、1秒、2秒后打印出了日志。此时达到了想要的效果。很棒!2. ref 定义的数据作为 prop 传递时
ref 定义数据为基本类型时
直接使用函数作为
source
参数,进行监听即可:App 组件
Child 组件
,此时由于ref
定义的是基本数据类型,所以也不存在是否需要深度监听的问题当 ref 定义数据为引用类型时
六、watch 监听 provide 提供的数据时
1. 提供的数据为 Proxy 时
2. 提供的数据为 ref 时
3. 提供的数据不是响应式数据时
七、监听多个数据
在实际开发过程中,可能会需要同时监听多个值,我们看一下多个值的情况,
watch
是如何处理以及响应的:可以再控制台看到,三次变化都会输出日志,并且
newValue
和oldValue
都是一个数组,里面值的顺序对应着source
里数组的顺序。八、竞态问题
在业务开发的过程中,时常面临这样的需求:监听某个数据的变化,当数据发生变化时,重新进行网络请求。下面写一段代码,来模拟这个需求:
可以看到界面上展示的结果是:返回的数据为李四,显然这不是我们想要的结果。最后一次是将
name
修改为了王五,所以肯定是希望返回的结果为王五。那出现这个异常的原因是什么呢?那么该如何解决呢?上面提到过,
watch
的callback
中具备第三个参数onCleanup
,我们来尝试着用一下:此时,在浏览器上,只会出现:返回的数据为王五。
九、watchEffect
上面说了很多 watch 的应用场景和常见问题。在需要监听多个数据时,可以使用数组作为 source。但是多个数据,如果是很多个呢,可能比较负责的逻辑,其中使用了较多的响应式数据,这个时候,使用 watch 去监听,显然不太适合。
这里可以使用新的 API:watchEffect
1. watchEffect API
watchEffect(effect, options)
effect
: 函数。内部依赖的响应式数据发生变化时,会触发effect
重新执行onCleanup
:形参,函数类型,接受一个回调函数。每次更新时,会调用上一次注册的onCleanup
函数。作用同 watch 中的 onCleanup 参数。options
:flush
:pre
|post
|sync
pre
:在组件更新前执行副作用;post
:在组件更新后运行副作用,可以使用watchPostEffect
替代;sync
:每个更改都强制触发watch
,可以使用watchSyncEffect
替代。onTrack
:函数,具备event
参数,调试用。将在响应式property
或ref
作为依赖项被追踪时被调用onTrigger
:函数,具备event
参数,调试用。将在依赖项变更导致副作用被触发时被调用。2. watchEffect 用法
2秒之后,查看控制台,发现打印了三次日志:
第一次是初始值
第二次是修改 name 触发的监听
第三次是修改 visible 触发的监听
而且每次打印的都是当前最新值
由此可以看出:
watchEffect 默认监听,也就是默认第一次就会执行;
不需要设置监听的数据,在 effect 函数中,用到了哪个数据,会自动进行依赖,因此不用担心类似 watch 中出现深层属性监听不到的问题;
只能获取到新值,由于没有提前指定监听的是哪个数据,所以不会提供旧值。
3. watchEffect 可能会出现的问题
在上面的用法中,感觉
watchEffect
使用起来还是很方便的,会自动依赖,而且还不用考虑各种深度依赖的问题。那watchEffect
会不会有什么陷阱需要注意呢?这次在
effect
函数中添加了异步任务,在setTimeout
中使用响应式数据,会发现,控制台一直都只展示一个日志:第一次进入时打印的。也就是说,在异步任务(无论是宏任务还是微任务)中进行的响应式操作,
watchEffect
无法正确的进行依赖收集。所以后面无论数据如何变更,都不会触发effect
函数。如果真的需要用到异步的操作,可以在外面先取值,再放到异步中去使用
修改之后,在控制台中可以正常的看到三次日志。
十、总结
当
wacth
的source
为Proxy
类型时:deep
属性失效,强制进行深度监听;新旧值指向同一个引用,导致内容是一样的。
当
watch
的source
是RefImpl
类型时:直接监听
state
和 监听() => state.value
是等效的;如果
ref
定义的是引用类型,并且想要进行深度监听,需要将deep
设置为true
。当
watch
的source
是函数时,可以监听到函数返回值的变更。如果想监听到函数返回值深层属性的变化,需要将deep
设置为true
;如果想监听多个值的变化,可以将
source
设置为数组,内部可以是Proxy
对象,可以是RefImpl
对象,也可以是具有返回值的函数;在监听组件
props
时,建议使用函数的方式进行watch
,并且希望该prop
深层任何属性的变化都能触发,可以将deep
属性设置为true
;使用
watchEffect
时,注意在异步任务中使用响应式数据的情况,可能会导致无法正确进行依赖收集。如果确实需要异步操作,可以在异步任务外先获取响应式数据,再将值放到异步任务里进行操作。十一:相关链接
watch监视ref定义的数据
watch监视reactive定义的数据
watch 时 value 的问题
watchEffect 函数
写在后面
如果有写的不对或不严谨的地方,欢迎大家能提出宝贵的意见,十分感谢。
如果喜欢或者有所帮助,欢迎 Star,对作者也是一种鼓励和支持。