likes
comments
collection
share

Vue中使用Watch的一些有趣的小测验

作者站长头像
站长
· 阅读数 5

vue3的文档中,watch可以侦听的数据源类型,可以是ref,响应式对象、getter函数、或者上述三种组成的数组。

粗略一看,这里好像没有什么复杂的。但是,如果ref的值如果是一个对象,如果computed返回是一个对象;在使用watch侦听时,如果又开启了deep,再结合不同的修改数据源的方式,watch会不会响应,可能一下就不会那么确定了。

对于非原始类型值的ref的开启deep侦听

操作开启deep是否响应?
直接修改.value
直接修改.value
修改.value的a属性
修改.value的a属性

例如,下面这段代码。objA是一个值为对象ref值,对于它分别添加一个开启和不开启deepwatch。再添加两个button,代表两种修改数据的方式:修改.value、修改.value属性a。分别点击两个button,哪个watch会响应,是不是可能就不那么确定了?

可以试着填写下上面的表格,然后再去演练场(记得打开调试console面板)试试验证下。

<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会响应么?一直点会一直响应么?

<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.valuecomputedObjRef 都会返回一个新对象,自然两个watch都会响应。

所以,在业务开发中,这种场景使用watch侦听是否开启deep是没有任何区别的。

直接给 watch() 传入一个响应式对象,会隐式地创建一个深层侦听器。那么,如果配置deep:false,深层属性的修改可以做到不触发侦听么?试一试

<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属性”会触发侦听么?试一试?

<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么? 试一试?

<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函数更合适的业务场景是需要计算或者监听响应式对象的某个属性时。如:

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配置。

总结

  1. 对于开启watchdeep配置,应该要有明确的业务场景和正确的目地,只有在下述场景需要手动配置开启:

    1. 侦听的数据源为非原始值的ref(或者返回一个对象的comouted)时;
    2. getter函数()返回响应式对象值为对象的属性,又希望深层响应时;
    3. 以上场景,修改整个.value或替换整个getter函数返回响应式对象值为对象的属性,即使内部属性没有变化,依旧会触发更新;
  2. 为了提高性能,满足业务场景的前提下,应该优先考虑使用shallowRefshallowReactive,从源头减少不需要的深层次侦听。

  3. 对于一些和理解中的有差异表现,应该要及时查阅文档、或做下小测验。

  4. 以上各种情况都只描述的表现为的行为,如果想知道为什么会有这些行为,那么是时候看vue的源码了