引言
前端开发中,想必不少开发者都在 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
属性,而非 watch
。computed
属性在依赖数据变化时会自动更新,并且是缓存的,只在依赖变化时重新计算。
<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 的产生。