主题
滥用watch。
名字解释
例子
先看一个代码例子:
<template>
{{ dataList }}
</template>
<script setup lang="ts">
import { ref, watch } from "vue";
const dataList = ref([]);
const props = defineProps(["disableList", "type", "id"]);
watch(
() => props.disableList,
() => {
// The logic based on disableList is very complex, and it computes a new list synchronously
const newList = getListFromDisabledList(dataList.value);
dataList.value = newList;
},
{ deep: true }
);
watch(
() => props.type,
() => {
// The logic based on type is very complex and computes a new list synchronously
const newList = getListFromType(dataList.value);
dataList.value = newList;
}
);
watch(
() => props.id,
() => {
// Fetch dataList from the server
fetchDataList();
},
{ immediate: true }
);
</script>
在上面的示例中,当props.id被更新或者初始化时候,dataList会从服务器异步获取。
当disableList,type更新时,将同步计算新的 dataList。
代码逻辑流程图如下:
乍一看,上面的代码似乎很好,但是当不熟悉这个模块的新同事接手时,问题就出现了。
通常,在接管我们不熟悉的业务领域时,我们需要找到一个起点。
对于前端方面哈,这个起点肯定是浏览器中呈现的页面。
在 Vue 中,页面是从模板渲染的,因此识别模板中使用的变量及其来源可以帮助我们理解业务逻辑。
以变量disableList为例,追溯其来源通常可以明确业务逻辑。
在我们的例子中,来源是多种多样的,且来自多个来源。
首先,它通过 watch props.id 从服务器异步更新。
然后,它通过监听watch props.disableList 和 watch props.type 上实现同步更新。
此时,不熟悉业务的同事收到产品要求以更新检索逻辑时,必须首先熟悉其多个来源背后的逻辑。
在了解了逻辑之后,他们需要准确分析哪些应该修改以满足产品要求。
然而,在实践中,在维护别人的代码(尤其是复杂代码)时,我们通常不愿意修改现有代码,而是在上面添加我们自己的代码。
更改别人的复杂代码可能会引入错误,然后我们可能会受到指责。因此,我们通常的方法是添加另一个并实现最新的业务逻辑:
watch(
() => props.xxx,
() => {
// Add the latest business logic
const newList = getListFromXxx(dataList.value);
dataList.value = newList;
}
);
经过几次迭代后,此文件变得杂乱无章,充斥着大量语句,从而导致“屎山代码”。当然,在某些情况下,这种编码风格可能是故意确保自己在团队中的地位,因为没有人敢碰这个复杂的代码块。。。。。。。
如何解决问题 computer
首先我们先明白正确的可维护的变量流程
dataList在模板中,同步更新,数据异步从服务器获取。
整个过程可以可视化为单个线程。当新开发人员加入团队迭代相关业务时,只需要了解最新的产品需求是建议在同步阶段还是异步阶段修改代码,然后在对应阶段添加最新的代码即可。
基于以上逻辑,代码简写如下:
<template>
{{ renderDataList }}
</template>
<script setup lang="ts">
import { ref, computed, watch } from "vue";
const props = defineProps(["disableList", "type", "id"]);
const dataList = ref([]);
const renderDataList = computed(() => {
// Calculate the list based on disableList
const newDataList = getListFromDisabledList(dataList.value);
// Calculate the list based on type
return getListFromType(newDataList);
});
watch(
() => props.id,
() => {
// Fetch dataList from the server
fetchDataList();
},
{
immediate: true,
}
);
</script>
在上面中,我们不再渲染dataList变量,而是渲染renderDataList变量。 是包含dataList的所有同步相关逻辑的 。代码逻辑流程图如下:
这样,如果有新需求变量变更,修改逻辑的演示如下:
const renderDataList = computed(() => {
// Add the latest business logic from the product
const xxxList = getListFromXxx(dataList.value);
// Calculate the list based on disableList
const newDataList = getListFromDisabledList(xxxList);
// Calculate the list based on type
return getListFromType(newDataList);
});
结论
本文介绍了两种主要的使用场景:一种是当被监视的值发生变化并需要同步更新要渲染的值时,另一种是当值发生变化并需要从服务器异步获取要渲染的值时。
如果同步和异步更新都不加区别地写在watch中,那么接手的后续维护者会发现梳理相关逻辑非常痛苦。
这是因为到处都在更新值,完全不清楚在哪里添加最新的业务逻辑。
随着时间的流逝,如果代码变成一堆实例,可维护性就会恶化。
我们的优化解决方案是将所有同步更新的代码堆叠到renderDataList
后续维护者只需要决定新业务是否是同步更新,那么他们就应该在dataList中写入新的业务逻辑。
如果是对 renderDataList的异步更新,则应将新的业务逻辑写入 computed中。
题外话
那么vue中 watch 和computer的区别你知道了吗?
1、缓存机制:
computed属性具有缓存机制。只有当其依赖的数据发生变化时,它才会重新计算。如果依赖的数据没有变化,那么它将直接返回之前缓存的结果。这种缓存机制可以提高性能,避免不必要的计算。
watch属性则没有缓存机制。每当它所监听的数据发生变化时,它都会触发相应的操作,不论这个操作是否已经被执行过。
2、异步支持:
computed不支持异步操作。如果computed内部包含异步操作,它将无法正常工作,因为它依赖于同步的响应式依赖。
watch则支持异步操作。在watch的回调函数中,你可以执行任何异步操作,如网络请求等。
3、执行时机:
computed属性在依赖的数据发生变化时才会执行。如果数据没有变化,那么它将不会执行。
watch属性则会在所监听的数据发生变化时立即执行其回调函数。
4、数据类型:
computed主要用于处理复杂的逻辑运算,返回一个新的属性值。它通常用于处理一些需要通过计算得到的数据,如根据购物车中的商品数量和单价计算出总价等。
watch则主要用于监听数据的变化,并在数据变化时执行一些副作用或异步操作。例如,根据用户输入的地址获取并显示地图信息等。
5、参数和返回值:
computed属性通常不需要额外的参数,它直接返回一个新的属性值。同时,它也可以定义一个setter函数来修改它的值。
watch属性则需要两个参数:newVal(新值)和oldVal(旧值)。在watch的回调函数中,你可以使用这两个参数来执行相应的操作。
6、性能:
由于computed具有缓存机制,因此它在处理大量数据或复杂计算时通常具有更好的性能。
watch则没有缓存机制,因此在处理大量数据或频繁变化的数据时可能会导致性能问题。