Vue 3 的 Computed 与 Watch:不同场景下的选择之道
大家好,今天我们来深入探讨 Vue 3 中两个非常重要的特性:computed
和 watch
。它们都用于响应式数据的变化,但适用场景却有所不同。理解它们的区别,并根据具体需求选择合适的方案,是编写高效、可维护的 Vue 应用的关键。
Computed:计算属性的魅力
computed
,顾名思义,是用于定义计算属性的。它本质上是一个派生值,其值依赖于其他响应式数据。当依赖的数据发生变化时,计算属性会自动重新计算。
核心特性:
- 缓存机制: 计算属性只有在其依赖的响应式数据发生变化时才会重新计算。如果依赖没有改变,则直接返回缓存的结果,避免不必要的计算开销。
- 声明式: 以声明的方式描述数据之间的关系,使代码更具可读性和可维护性。
- 同步执行: 计算属性的计算是同步的,这意味着它会立即返回结果。
基本用法:
<template>
<div>
<p>原始价格: {{ price }}</p>
<p>折扣: {{ discount }}</p>
<p>最终价格: {{ finalPrice }}</p>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const price = ref(100);
const discount = ref(0.1);
const finalPrice = computed(() => {
return price.value * (1 - discount.value);
});
return {
price,
discount,
finalPrice,
};
},
};
</script>
在这个例子中,finalPrice
是一个计算属性,它依赖于 price
和 discount
。当 price
或 discount
的值发生变化时,finalPrice
会自动更新。
更复杂的例子:包含 getter 和 setter
计算属性还可以包含 getter 和 setter,允许我们不仅读取计算属性的值,还可以设置它的值。这在某些需要双向绑定的场景中非常有用。
<template>
<div>
<p>全名: {{ fullName }}</p>
<input type="text" v-model="fullName">
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
const fullName = computed({
get: () => {
return firstName.value + ' ' + lastName.value;
},
set: (newValue) => {
const names = newValue.split(' ');
firstName.value = names[0] || '';
lastName.value = names[1] || '';
},
});
return {
firstName,
lastName,
fullName,
};
},
};
</script>
在这个例子中,fullName
计算属性既可以读取全名,也可以通过修改 fullName
的值来更新 firstName
和 lastName
。
何时使用 Computed:
- 数据转换: 将原始数据转换为另一种形式,例如格式化日期、计算总和等。
- 数据过滤: 根据某些条件过滤数据,例如筛选出符合特定条件的用户。
- 派生状态: 根据其他状态派生新的状态,例如根据用户的登录状态显示不同的界面。
- 简化模板: 将复杂的表达式逻辑封装到计算属性中,使模板更简洁易懂。
- 双向绑定: 需要对计算属性进行赋值操作时。
Watch:监听者的力量
watch
用于监听一个或多个响应式数据的变化,并在数据变化时执行回调函数。它更像是一个观察者模式的实现。
核心特性:
- 灵活性: 可以监听单个数据、多个数据、甚至是一个函数返回的值。
- 异步执行: 回调函数默认是异步执行的,这意味着它不会阻塞主线程。可以通过
flush: 'sync'
选项来强制同步执行,但不建议这样做,除非有特殊需求。 - 深度监听: 可以通过
deep: true
选项来深度监听对象或数组的变化,包括嵌套属性的变化。 - 立即执行: 可以通过
immediate: true
选项在组件挂载后立即执行一次回调函数。
基本用法:
<template>
<div>
<p>计数器: {{ count }}</p>
<button @click="increment">增加</button>
</div>
</template>
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const count = ref(0);
const increment = () => {
count.value++;
};
watch(
count,
(newValue, oldValue) => {
console.log('计数器从', oldValue, '变为', newValue);
// 在这里执行副作用操作,例如发送网络请求、更新 DOM 等
}
);
return {
count,
increment,
};
},
};
</script>
在这个例子中,watch
监听了 count
的变化,并在 count
的值发生变化时,在控制台输出日志。
监听多个数据:
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const firstName = ref('John');
const lastName = ref('Doe');
watch(
[firstName, lastName],
(newValues, oldValues) => {
console.log('firstName 从', oldValues[0], '变为', newValues[0]);
console.log('lastName 从', oldValues[1], '变为', newValues[1]);
}
);
return {
firstName,
lastName,
};
},
};
</script>
在这个例子中,watch
监听了 firstName
和 lastName
的变化,并在其中任何一个值发生变化时,输出相应的日志。注意,回调函数接收的 newValues
和 oldValues
是一个数组,分别对应监听的数据的新值和旧值。
深度监听:
<script>
import { ref, watch } from 'vue';
export default {
setup() {
const user = ref({
name: 'John',
address: {
city: 'New York',
},
});
watch(
user,
(newValue, oldValue) => {
console.log('user 对象发生变化', newValue, oldValue);
},
{ deep: true }
);
const changeCity = () => {
user.value.address.city = 'Los Angeles';
};
return {
user,
changeCity,
};
},
};
</script>
在这个例子中,deep: true
选项开启了深度监听,这意味着即使 user.address.city
的值发生变化,也会触发 watch
的回调函数。
何时使用 Watch:
- 副作用操作: 需要在数据变化时执行某些副作用操作,例如发送网络请求、更新 DOM、操作本地存储等。
- 异步操作: 需要在数据变化时执行异步操作,例如延迟加载数据、执行动画等。
- 复杂逻辑: 需要在数据变化时执行复杂的逻辑,无法简单地通过计算属性来表达。
- 监听路由变化: 监听
$route
对象的变化,以便根据不同的路由执行不同的操作。 - 与外部库集成: 与外部库集成,需要在数据变化时调用外部库的 API。
- 数据持久化: 将数据的变化同步到本地存储或服务器。
Computed vs Watch:对比与选择
特性 | Computed | Watch |
---|---|---|
目的 | 计算派生值 | 监听数据变化并执行副作用 |
缓存 | 有缓存机制 | 无缓存机制 |
执行 | 同步执行 | 默认异步执行 |
适用场景 | 数据转换、数据过滤、派生状态、简化模板等 | 副作用操作、异步操作、复杂逻辑、与外部库集成等 |
返回值 | 返回计算后的值 | 无返回值 (void),关注的是过程 |
声明方式 | 声明式 | 命令式 |
选择原则:
- 如果需要根据现有数据计算出一个新的值,并且这个值可能会在模板中使用,那么应该使用
computed
。 - 如果需要在数据变化时执行某些副作用操作,例如发送网络请求、更新 DOM 等,那么应该使用
watch
。 - 如果逻辑比较复杂,无法简单地通过
computed
来表达,或者涉及到异步操作,那么应该使用watch
。
一些具体的例子:
-
显示用户全名:
computed
是更好的选择,因为它只是简单地将firstName
和lastName
拼接起来。const fullName = computed(() => firstName.value + ' ' + lastName.value);
-
根据用户输入搜索数据:
computed
可以用于过滤数据,但watch
更适合处理搜索请求,因为它可能涉及到异步操作,例如发送网络请求。// 使用 computed 过滤数据 const filteredData = computed(() => { return data.value.filter(item => item.name.includes(searchTerm.value)); }); // 使用 watch 发送搜索请求 watch(searchTerm, (newValue) => { // 发送网络请求,获取搜索结果 fetchData(newValue); });
-
在数据变化时更新第三方图表:
watch
是更好的选择,因为它需要执行副作用操作(更新图表)。watch(data, (newValue) => { // 调用第三方图表库的 API,更新图表 updateChart(newValue); });
-
在用户登录成功后跳转到首页:
watch
是更好的选择,因为它需要监听用户登录状态的变化,并执行跳转操作。watch(isLoggedIn, (newValue) => { if (newValue) { router.push('/'); } });
最佳实践
- 尽量使用
computed
来处理数据转换和派生状态,避免在模板中编写复杂的表达式。 - 避免在
watch
的回调函数中执行大量的计算操作,尽量将计算逻辑放到computed
中。 - 使用
deep: true
选项时要谨慎,因为它会带来性能开销。尽量只在必要时才开启深度监听。 - 在组件销毁时,取消
watch
的监听,避免内存泄漏。(Vue 3会自动处理,不需要手动取消) - 合理使用
immediate: true
选项,避免不必要的初始执行。 - 如果需要监听多个数据的变化,尽量将它们放到一个数组中,然后使用
watch
监听这个数组。 - 避免在
computed
的 setter 中修改其他响应式数据,这可能会导致循环依赖。 - 尽量保持
computed
和watch
的回调函数简洁明了,避免包含过多的业务逻辑。 - 使用
watch
时,注意处理竞态条件,例如在发送网络请求时,取消之前的请求。
性能考量
computed
的缓存机制使其在性能方面通常优于 watch
,尤其是在计算量大的情况下。 然而,watch
提供了更大的灵活性,在某些场景下是不可替代的。
- Computed: 因为有缓存,所以除非依赖发生改变,否则不会重新计算。这意味着对于相同的输入,
computed
只会计算一次。 - Watch: 每次监听的数据发生改变,都会执行回调函数。如果监听的数据频繁变化,
watch
的性能开销会比较大。特别是使用了deep: true
,会进行深度比较,开销更大。
因此,在选择 computed
和 watch
时,需要权衡性能和灵活性。如果性能是关键因素,并且可以使用 computed
来实现需求,那么应该优先选择 computed
。
示例:更复杂的场景
假设我们需要开发一个购物车功能。我们需要显示购物车中的商品总价,并且需要在商品数量发生变化时更新总价。
<template>
<div>
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - {{ item.price }} x
<input type="number" v-model.number="item.quantity">
</li>
</ul>
<p>总价: {{ totalPrice }}</p>
</div>
</template>
<script>
import { ref, computed } from 'vue';
export default {
setup() {
const cart = ref([
{ id: 1, name: 'Apple', price: 1, quantity: 2 },
{ id: 2, name: 'Banana', price: 0.5, quantity: 3 },
]);
const totalPrice = computed(() => {
return cart.value.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
});
return {
cart,
totalPrice,
};
},
};
</script>
在这个例子中,totalPrice
使用 computed
来计算购物车中的商品总价。当 cart
中的任何一个商品的 quantity
发生变化时,totalPrice
会自动更新。
现在,假设我们需要在总价超过 100 元时显示一个提示信息。我们可以使用 watch
来监听 totalPrice
的变化,并在总价超过 100 元时显示提示信息。
<template>
<div>
<ul>
<li v-for="item in cart" :key="item.id">
{{ item.name }} - {{ item.price }} x
<input type="number" v-model.number="item.quantity">
</li>
</ul>
<p>总价: {{ totalPrice }}</p>
<p v-if="showWarning">总价超过 100 元,请注意!</p>
</div>
</template>
<script>
import { ref, computed, watch } from 'vue';
export default {
setup() {
const cart = ref([
{ id: 1, name: 'Apple', price: 1, quantity: 2 },
{ id: 2, name: 'Banana', price: 0.5, quantity: 3 },
]);
const totalPrice = computed(() => {
return cart.value.reduce((total, item) => {
return total + item.price * item.quantity;
}, 0);
});
const showWarning = ref(false);
watch(totalPrice, (newValue) => {
showWarning.value = newValue > 100;
});
return {
cart,
totalPrice,
showWarning,
};
},
};
</script>
在这个例子中,我们使用 computed
来计算 totalPrice
,使用 watch
来监听 totalPrice
的变化,并在总价超过 100 元时更新 showWarning
的值。
总结
computed
和 watch
是 Vue 3 中强大的工具,用于响应式数据的变化。 理解它们的区别和适用场景,并根据具体需求选择合适的方案,可以编写出高效、可维护的 Vue 应用。
快速回顾与应用建议
总结一下,computed
适合数据转换和派生,watch
适合副作用操作。选择时要考虑性能和灵活性,并根据具体场景进行权衡。希望今天的讲解能够帮助大家更好地理解和使用 computed
和 watch
。