在vue3
的文档中,watch
可以侦听的数据源类型,可以是ref
,响应式对象、getter
函数、或者上述三种组成的数组。
粗略一看,这里好像没有什么复杂的。但是,如果ref
的值如果是一个对象,如果computed
返回是一个对象;在使用watch
侦听时,如果又开启了deep
,再结合不同的修改数据源的方式,watch
会不会响应,可能一下就不会那么确定了。
对于非原始类型值的ref的开启deep侦听
操作 | 开启deep | 是否响应? |
---|---|---|
直接修改.value | 否 | |
直接修改.value | 是 | |
修改.value的a属性 | 否 | |
修改.value的a属性 | 是 |
例如,下面这段代码。objA
是一个值为对象ref
值,对于它分别添加一个开启和不开启deep
的watch
。再添加两个button
,代表两种修改数据的方式:修改.value
、修改.value属性a
。分别点击两个button
,哪个watch
会响应,是不是可能就不那么确定了?
可以试着填写下上面的表格,然后再去演练场(记得打开调试console面板)试试验证下。
vue
<script setup>
import { ref, watch } from 'vue';
const objA = ref({ a: 1 });
watch(objA, (val, oldVal) => {console.log('我是没有开启deep的监听,响应了', val, oldVal)});
watch(
objA,
(val, oldVal) => { console.log('我是开启deep的监听,响应了', val, oldVal) },
{ deep: true }
);
const changePropA = () => { objA.value.a = 2 };
const changeRef = () => { objA.value = { a: 1 } };
</script>
<template>
<button @click="changeRef">修改.value</button>
<br />
<button @click="changePropA">修改.value属性a</button>
</template>
对于上述的四种情况,会不会响应,可能只有"开启deep
时,直接修改.value
(且对象内属性值没有改变)"这一种场景会让人有点混乱。有童鞋可能会认为,监听一个值为对象的ref
时,开启deep
,只有在深层次的属性值发生改变时才会响应,但这里其实是,只要开启deep
,属性和整个.value
发生改变时,都会响应watch
。
有时候我们会对vue
的表现有一些错误的理解,这是常见的。比如我,一直以为ref
只有通过.value
修改时,才会触发数据响应;很长时间都不知道,当ref的值不是一个原始类型时,会使用reactive
转换为一个响应式对象,修改属性也能触发数据响应。
所以在vue
的使用,当出现一些和理解不一致的表现时,应该要及时查阅文档、做下小测验,把问题弄懂才能避免产生更多问题、避免浪费更多Debug时间。
当computed基于一个非原始类型的值返回一个对象时?
再来看下这段点击"修改值"按钮时,watch
会响应么?一直点会一直响应么?
vue
<script setup>
import { ref, watch, computed } from 'vue';
const objRef = ref({ a: 1, b:2 });
const computedObjRef = computed(() => ({ ...objRef.value }));
watch(computedObjRef, (val, oldVal) => {console.log('未开启deep的监听响应了', val, oldVal);});
watch(
computedObjRef,
(val, oldVal) => {console.log('开启deep的监听响应了', val, oldVal)},
{ deep: true }
);
const changeRef = () => {
objRef.value = { a: 1,b:2 };
};
</script>
<template>
<button @click="changeRef">修改值</button>
</template>
事实是,这里不仅两个watch
都会响应,而且,一直点会一直响应。试一试?
为什么,因为这里上面代码和对对非原始类型值的ref
的开启deep
侦听一样,对对非原始类型值的ref
的开启deep
侦听时,修改某个属性或者修改整个value
的对象值(即使深层属性未变),都会触发侦听。
而每次修改objRef.value
,computedObjRef
都会返回一个新对象,自然两个watch
都会响应。
所以,在业务开发中,这种场景使用watch
侦听是否开启deep
是没有任何区别的。
直接给 watch()
传入一个响应式对象,会隐式地创建一个深层侦听器。那么,如果配置deep:false
,深层属性的修改可以做到不触发侦听么?试一试
vue
<script setup>
import { reactive, watch } from 'vue';
const objRef = reactive({ a: { b: 1 } });
watch(
objRef,
(val, oldVal) => {
console.log('我是没有开启deep的监听,响应了', val, oldVal);
},
{ deep: false }
);
watch(objRef, (val, oldVal) => {
console.log('我是开启deep的监听,响应了', val, oldVal);
});
const changePropA = () => {
objRef.a.b = objRef.a.b % 2 ? 2 : 1;
};
const changeRef = () => {
objRef.a = { b: 2 };
};
</script>
<template>
<button @click="changeRef">修改a对象</button>
<br /><br /><br />
<button @click="changePropA">修改a.b属性</button>
</template>
答案是不行,想要实现上述的目的,只能使用shallowReactive
方式实现。
再比较下面这段代码,点击"修改a.b属性"和"修改.value属性"会触发侦听么?试一试?
vue
<script setup>
import { ref, watch } from 'vue';
const objRef = ref({ a: { b: 1 } });
watch(objRef.value, (val, oldVal) => {
console.log('未手动开启deep的监听,响应了', val, oldVal);
});
const changePropA = () => {
objRef.value.a.b = objRef.value.a.b % 2 ? 2 : 1;
};
const changeRef = () => {
objRef.value = { a: { b: 1 } }
};
</script>
<template>
<button @click="changePropA">修改a.b属性</button>
<button @click="changeRef">修改.value属性</button>
</template>
这里,点击"修改a.b属性"是会触发侦听的,因为watch
监听的是objRef.value
,而objRef.value
就是一个响应式对象,所以会默认开启深层次监听。
而点击"修改.value属性"并不会触发侦听,因为侦听的还是之前的objRef.value
对应的响应式对象并没有变,只是objRef.value
变成了一个新的响应式对象而已。所以,点击"修改.value属性"后,再点击"修改a.b属性",也不会触发响应了。
什么时候需要使用getter函数侦听?
再看看下面这段代码,侦听一个返回非原始类型值的ref
的值getter
函数。这种时候,只修改深层次的属性,会触发watch
么? 试一试?
vue
<script setup>
import { ref, watch, computed } from 'vue';
const objRef = ref({ a: 1, b:2 });
watch(
()=>objRef.value,
(val, oldVal) => {console.log('开启deep的监听响应了', val, oldVal)},
);
const changeA = () => {
objRef.value.a = objRef.value.a % 2 === 1 ? 2 : 1;
};
</script>
<template>
objRef:{{objRef}}
<button @click="changeA">修改属性a</button>
</template>
答案是不会。未开启deep
的情况下,getter函数
只有在返回不同值或对象才会从触发侦听。
其实,对于getter
函数更合适的业务场景是需要计算或者监听响应式对象的某个属性时。如:
vue
const a = ref(0)
const b = ref(0)
// 监听两个ref的值相加
watch(
() => x.value + y.value,
(val) => {
console.log(`a+b等于: ${sum}`)
}
)
const state = reactive({
a: 1,
b: {a:1}
})
// 监听响应式对象的某个属性
watch(
() => state.b,
(v) => {
console.log(`state.b变化了: ${v}`)
}
)
注意,对于getter函数() => state.b
,只有state.b
整个被替换时才会触发侦听。如果这里这里想侦听深层次的属性变化,需要开启deep
配置。
总结
-
对于开启
watch
的deep
配置,应该要有明确的业务场景和正确的目地,只有在下述场景需要手动配置开启:- 侦听的数据源为非原始值的
ref
(或者返回一个对象的comouted
)时; getter函数()
返回响应式对象值为对象的属性,又希望深层响应时;- 以上场景,修改整个
.value
或替换整个getter函数返回响应式对象值为对象的属性,即使内部属性没有变化,依旧会触发更新;
- 侦听的数据源为非原始值的
-
为了提高性能,满足业务场景的前提下,应该优先考虑使用
shallowRef
、shallowReactive
,从源头减少不需要的深层次侦听。 -
对于一些和理解中的有差异表现,应该要及时查阅文档、或做下小测验。
-
以上各种情况都只描述的表现为的行为,如果想知道为什么会有这些行为,那么是时候看vue的源码了