没想到 Vue 中滥用 watch 危害这么大!吓得我连夜修改代码

简介: 没想到 Vue 中滥用 watch 危害这么大!吓得我连夜修改代码

引言



前端开发中,想必不少开发者都在 Vue 中使用过 watch 函数。但是你知道如果滥用 watch,会带来怎样的后果吗?今天我们一起聊聊这个问题,并去解决它!


使用watch的错误案例

简单例子

我们先来看一个简单的例子,这里我们有一个组件,需要在 props 变化时执行一些操作:

<template>
  <div>{{ dataList }}</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps(['value']);
const dataList = ref([]);
watch(
  () => props.value,
  (newValue, oldValue) => {
    // 根据新的 props.value 更新 dataList
    dataList.value = processValue(newValue);
  }
);
function processValue(value) {
  // 处理值的逻辑
  return value.split(',');
}
</script>

乍一看,这段代码似乎没什么问题,但随着业务逻辑的复杂化,watch 的数量和复杂度会迅速增加。

复杂业务逻辑

假设现在我们需要根据多个 props 的变化更新 dataList

<template>
  <div>{{ dataList }}</div>
</template>
<script setup>
import { ref, watch } from 'vue';
const props = defineProps(['disableList', 'type', 'id']);
const dataList = ref([]);
watch(
  () => props.disableList,
  (newDisableList) => {
    dataList.value = processDisableList(newDisableList);
  },
  { deep: true }
);
watch(
  () => props.type,
  (newType) => {
    dataList.value = processType(newType);
  }
);
watch(
  () => props.id,
  (newId) => {
    fetchDataList(newId).then(newDataList => {
      dataList.value = newDataList;
    });
  },
  { immediate: true }
);
function processDisableList(disableList) {
  // 处理 disableList 的逻辑
  return disableList.map(item => item.id);
}
function processType(type) {
  // 处理 type 的逻辑
  return type.split(',');
}
function fetchDataList(id) {
  // 从服务器获取 dataList
  return fetch(`/api/dataList/${id}`).then(response => response.json());
}
</script>

在这个例子中,dataList 由多个 watch 控制,这些 watch 可能会相互影响,导致数据更新逻辑混乱。

 

使用watch带来的问题

逻辑分散

使用多个 watch 导致业务逻辑分散在多个函数中,难以维护。新同事接手时,往往需要花费大量时间理清各个 watch 之间的关系。

性能问题

每次 props 变化都会触发 watch,可能会带来性能问题,特别是在复杂的计算逻辑和频繁的数据更新中。

难以调试

watch 的回调函数可能会在意想不到的时机触发,导致调试变得困难,特别是在处理异步操作时。


解决滥用watch的方案

仅在必要时使用

watch 最主要的用途是监听特定数据变化并执行副作用操作。应尽量避免在 watch 中执行复杂的计算和业务逻辑。

使用 computed 替代

对于纯粹的计算逻辑,应使用 computed 属性,而非 watchcomputed 属性在依赖数据变化时会自动更新,并且是缓存的,只在依赖变化时重新计算。

<template>
  <div>{{ processedData }}</div>
</template>
<script setup>
import { ref, computed } from 'vue';
const props = defineProps(['value']);
const data = ref(props.value);
const processedData = computed(() => {
  return data.value.split(',');
});
</script>

使用组合函数(Composition Functions)

在复杂的业务逻辑中,使用组合函数可以将逻辑模块化,使代码更易于维护和重用。

<template>
  <div>{{ processedData }}</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue';
const props = defineProps(['disableList', 'type', 'id']);
const dataList = ref([]);
const disableList = ref(props.disableList);
const type = ref(props.type);
const id = ref(props.id);
const processedData = useDataProcessing(disableList, type, id);
function useDataProcessing(disableList, type, id) {
  const data = ref([]);
  const processedDisableList = computed(() => {
    return disableList.value.map(item => item.id);
  });
  const processedType = computed(() => {
    return type.value.split(',');
  });
  watch(
    id,
    (newId) => {
      fetchDataList(newId).then(newDataList => {
        data.value = newDataList;
      });
    },
    { immediate: true }
  );
  return computed(() => {
    return processFinalData(processedDisableList.value, processedType.value, data.value);
  });
}
function fetchDataList(id) {
  return fetch(`/api/dataList/${id}`).then(response => response.json());
}
function processFinalData(disableList, type, dataList) {
  // 处理最终数据的逻辑
  return dataList.filter(item => !disableList.includes(item.id) && type.includes(item.type));
}
</script>

案例实践

案例背景

假设我们有一个电商平台,需要根据用户选择的过滤条件动态更新商品列表。用户可以选择不同的分类、价格区间以及排序方式。

错误的实现方式

下面是一个过度使用 watch 的实现方式:

<template>
  <div>
    <Filter @change="handleFilterChange" />
    <ProductList :products="filteredProducts" />
  </div>
</template>
<script setup>
import { ref, watch } from 'vue';
import Filter from './Filter.vue';
import ProductList from './ProductList.vue';
const filters = ref({
  category: '',
  priceRange: [0, 100],
  sortBy: 'price'
});
const products = ref([]);
const filteredProducts = ref([]);
watch(
  () => filters.value.category,
  (newCategory) => {
    filteredProducts.value = filterProducts(products.value, filters.value);
  }
);
watch(
  () => filters.value.priceRange,
  (newPriceRange) => {
    filteredProducts.value = filterProducts(products.value, filters.value);
  }
);
watch(
  () => filters.value.sortBy,
  (newSortBy) => {
    filteredProducts.value = sortProducts(filteredProducts.value, filters.value.sortBy);
  }
);
function filterProducts(products, filters) {
  // 根据 filters 过滤产品列表的逻辑
  return products.filter(product => {
    return product.category === filters.category && product.price >= filters.priceRange[0] && product.price <= filters.priceRange[1];
  });
}
function sortProducts(products, sortBy) {
  // 根据 sortBy 对产品列表排序的逻辑
  return products.sort((a, b) => {
    if (sortBy === 'price') {
      return a.price - b.price;
    } else if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    }
  });
}
</script>

优化的实现方式

下面是使用 computed 和组合函数的优化实现方式:

<template>
  <div>
    <Filter @change="handleFilterChange" />
    <ProductList :products="filteredProducts" />
  </div>
</template>
<script setup>
import { ref, computed } from 'vue';
import Filter from './Filter.vue';
import ProductList from './ProductList.vue';
const filters = ref({
  category: '',
  priceRange: [0, 100],
  sortBy: 'price'
});
const products = ref([]);
const filteredProducts = useFilteredProducts(products, filters);
function useFilteredProducts(products, filters) {
  const filtered = computed(() => {
    return filterProducts(products.value, filters.value);
  });
  const sorted = computed(() => {
    return sortProducts(filtered.value, filters.value.sortBy);
  });
  return sorted;
}
function filterProducts(products, filters) {
  // 根据 filters 过滤产品列表的逻辑
  return products.filter(product => {
    return product.category === filters.category && product.price >= filters.priceRange[0] && product.price <= filters.priceRange[1];
  });
}
function sortProducts(products, sortBy) {
  // 根据 sortBy 对产品列表排序的逻辑
  return products.sort((a, b) => {
    if (sortBy === 'price') {
      return a.price - b.price;
    } else if (sortBy === 'name') {
      return a.name.localeCompare(b.name);
    }
  });
}
</script>


总结

在实际开发中,我们应尽量减少对 watch 的依赖,更多地使用 computed 来处理数据的同步更新。如果确实需要监听复杂的业务逻辑变化,可以考虑将逻辑模块化,通过组合函数实现,这样不仅使代码更清晰,还能有效减少 bug 的产生。

相关文章
|
6月前
|
JavaScript
Vue中如何实现兄弟组件之间的通信
在Vue中,兄弟组件可通过父组件中转、事件总线、Vuex/Pinia或provide/inject实现通信。小型项目推荐父组件中转或事件总线,大型项目建议使用Pinia等状态管理工具,确保数据流清晰可控,避免内存泄漏。
560 2
|
5月前
|
缓存 JavaScript
vue中的keep-alive问题(2)
vue中的keep-alive问题(2)
461 137
|
9月前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
1010 0
|
9月前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
8月前
|
人工智能 JSON JavaScript
VTJ.PRO 首发 MasterGo 设计智能识别引擎,秒级生成 Vue 代码
VTJ.PRO发布「AI MasterGo设计稿识别引擎」,成为全球首个支持解析MasterGo原生JSON文件并自动生成Vue组件的AI工具。通过双引擎架构,实现设计到代码全流程自动化,效率提升300%,助力企业降本增效,引领“设计即生产”新时代。
601 1
|
8月前
|
JavaScript 安全
在 Vue 中,如何在回调函数中正确使用 this?
在 Vue 中,如何在回调函数中正确使用 this?
414 0
|
9月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
541 1
|
11月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
1261 4
|
10月前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
1359 78
|
11月前
|
缓存 JavaScript 前端开发
Vue 基础语法介绍
Vue 基础语法介绍