在 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多平台发布


Miniwa
29 声望1 粉丝