在 Vue 3 中,watch 是一个强大的工具,适合监视响应式数据的变化并处理副作用逻辑。最近在做CodeReview的时候,发现了一些对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,
() => {
// 基于disableList的逻辑非常复杂,它同步计算一个新列表
const newList = getListFromDisabledList(dataList.value);
dataList.value = newList;
},
{ deep: true }
);
watch(
() => props.type,
() => {
// 基于类型的逻辑非常复杂,同步计算新列表
const newList = getListFromType(dataList.value);
dataList.value = newList;
}
);
watch(
() => props.id,
() => {
// 从数据库拉取数据
fetchDataList();
},
{ immediate: true }
);
</script>
在这个例子中,dataList 在模板中渲染。更新 props.id 和初始化时,会异步从服务器获取 dataList。
更新 props.disableList 和 props.type 时,会同步计算新的 dataList。
代码逻辑流程图如下:
乍一看,上面的代码可能没什么问题,但当不熟悉这方面的新同事接手时,问题就来了。
通常,在接手一个我们并不熟悉的业务领域时,我们需要找到一个起点。对于前端来说,这个起点肯定是浏览器中的渲染页面。从模版中我们可以知道 dataList 变量是核心所在,它有多个来源。
首先,服务器通过对 props.id 的监视进行异步更新。然后,通过对 props.disableList 和 props.type 的监视同步更新。
此时,不熟悉业务的同事如果收到要更新检索 dataList 逻辑的产品需求,就必须首先熟悉其多个来源背后的逻辑。
那么就要去看getListFromDisabledList、getListFromType例复杂的代码,分析清楚应该修改哪块才能满足产品要求。
不过,在实际操作中,当维护别人的代码(尤其是复杂的代码)时,我们一般不喜欢修改现有的代码,而是在上面添加自己的代码。更改他人的复杂代码很可能会引入错误(特别是有一些摸不着头脑的逻辑时),而我们也可能会因此造成生产事故。因此,我们通常的做法是添加另一个监视器,并在那里实现 dataList 的最新业务逻辑:
watch(
() => props.xxx,
() => {
// Add the latest business logic
const newList = getListFromXxx(dataList.value);
dataList.value = newList;
}
);
经过多次迭代后,这个 vue 文件就会变得杂乱无章,其中包含大量的观察语句,从而导致 "意大利面条代码"(Spaghetti Code 是一个编程术语,用于描述结构混乱、难以理解和维护的代码)。又或许这种编码风格可能是为了假装提高自己在团队中的价值,确保自己在团队中的地位,没有其他人敢碰这个复杂的代码。哈哈。
破局
上面这个例子,实际上是由于dataList的变更源过多引起的,而且里面还包含同步和异步两种。我们可以增加computed,把同步的变更整合到computed里,只保留异步的变更:
<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(() => {
const newDataList = getListFromDisabledList(dataList.value);
return getListFromType(newDataList);
});
watch(
() => props.id,
() => {
fetchDataList();
},
{
immediate: true,
}
);
</script>
我们不再渲染 dataList 变量,而是渲染 renderDataList。代码逻辑流程图如下:
当新的团队成员收到迭代 dataList 相关业务的产品需求时,由于我们的整个业务逻辑现在已经变成了一个线性序列,因此新的团队成员可以快速理清业务逻辑。
然后,根据产品的要求,他们可以决定是修改同步逻辑还是修改异步逻辑。以下是修改同步逻辑的演示:
const renderDataList = computed(() => {
// 添加最新的处理逻辑
const xxxList = getListFromXxx(dataList.value);
const newDataList = getListFromDisabledList(xxxList);
return getListFromType(newDataList);
});
总结
我们应该更多地使用 computed 来处理同步逻辑,将异步逻辑保留在 watch 中的方法。
computed 本身具备缓存特性,通过使用computed,我们可以减少状态的数量,因为computed是计算属性,是一个中间结果,是因变量不是自变量。
- 滥用 watch 会导致代码难以维护: 当 watch 被用于处理多个复杂的同步和异步更新时,会导致代码变得杂乱无章,给维护和理解业务逻辑带来困难。
- computed 和 watch 的适当使用可以提高代码质量: 将同步逻辑放入 computed 中,这样可以将业务逻辑线性化,使得代码更加清晰。同时,将异步逻辑保留在 watch 中,可以更好地区分和管理不同类型的更新。
- 优化后的代码结构有助于新成员快速上手: 通过将同步逻辑集中在 computed 中,新成员可以快速定位需要修改的业务逻辑,提高开发效率和代码的可维护性。
- 代码逻辑应该是线性和可追溯的: 代码逻辑应该像一个线程一样,从而使得业务逻辑的流向清晰明了,避免复杂的逻辑分叉和嵌套,这有助于代码的长期维护。
本文由mdnice多平台发布
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。