深入理解 Vue 3 中的计算属性与侦听器:联系、区别及与函数的对比
摘要: 在 Vue 3 的响应式系统中,计算属性(Computed)和侦听器(Watch)是处理数据逻辑的两大核心利器。虽然它们有时可以实现相似的功能,但其设计理念和使用场景却大相径庭。本文将深入剖析计算属性与侦听器的联系与核心区别,并进一步探讨它们与普通方法(Function)的不同,帮助你如何在项目中做出最合适的选择。
一、计算属性(Computed)
计算属性,顾名思义,是基于它们依赖的响应式数据动态计算得出的值。
1.1 核心特性
声明式依赖追踪: 计算属性会自动追踪其内部使用的所有响应式依赖。只有当依赖发生变化时,它才会重新计算。这是一种“惰性求值”的机制。
缓存机制: 这是计算属性最核心的优势。只要依赖项没有变化,多次访问计算属性会立即返回之前缓存的结果,而不会重新执行函数。
返回值: 计算属性必须返回一个值,并且这个值可以被用在模板中,就像普通的
data属性一样。
1.2 语法(Composition API)
在 Vue 3 的 Composition API 中,我们使用 computed 函数。
import { ref, computed } from 'vue';const firstName = ref('张');
const lastName = ref('三');// 写法一:传入一个 getter 函数
const fullName = computed(() => {console.log('计算属性 fullName 被重新计算了!');return `${firstName.value} ${lastName.value}`;
});// 写法二:使用包含 get 和 set 函数的对象(可写计算属性)
const writableFullName = computed({get() {return `${firstName.value} ${lastName.value}`;},set(newValue) {[firstName.value, lastName.value] = newValue.split(' ');}
});二、侦听器(Watch)
侦听器用于观察和响应特定数据源的变化,并在变化时执行副作用(Side Effects)。
2.1 核心特性
副作用处理: 侦听器的核心目的是执行副作用,例如:发起异步请求、操作 DOM、更改其他状态等。
无返回值: 侦听器函数不返回任何值,它的价值在于执行过程,而非产生一个新值。
更底层、更灵活: 它可以监听一个或多个数据源,并获取变化前后的值,让你能够执行任何复杂的逻辑。
2.2 语法(Composition API)
使用 watch 或 watchEffect 函数。
import { ref, watch, watchEffect } from 'vue';const count = ref(0);
const anotherState = ref('');// 1. watch:显式指定侦听源和回调
watch(count, (newValue, oldValue) => {console.log(`count 从 ${oldValue} 变更为 ${newValue}`);// 可以在这里发起 Ajax 请求,或者做其他任何事情
});// 监听多个源
watch([count, anotherState], ([newCount, newState], [oldCount, oldState]) => {// 处理逻辑
});// 2. watchEffect:自动追踪其同步执行过程中的依赖
watchEffect(() => {console.log(`count 的值是:${count.value}, 自动被追踪!`);// 当 count.value 变化时,这个函数会重新执行
});三、计算属性 vs 侦听器
3.1 核心区别对比表
| 特性 | 计算属性 (Computed) | 侦听器 (Watch) |
|---|---|---|
| 设计目的 | 基于依赖生成一个新的派生值 | 在依赖变化时执行副作用 |
| 返回值 | 必须返回一个值 | 没有返回值 |
| 缓存 | 有缓存,依赖不变则不重新计算 | 无缓存,变化即执行 |
| 异步操作 | 不支持在 getter 函数内进行异步操作 | 支持,是处理异步操作的理想场所 |
| 使用场景 | 模板中需要渲染的、经过复杂计算的数据 | 数据变化时需要执行的操作(如请求API、验证) |
3.2 联系与选择
联系: 它们都是 Vue 响应式系统的重要组成部分,用于响应数据变化。
如何选择?
使用计算属性的场景:
你需要根据一个或多个响应式数据计算出一个新值,并希望在模板中像普通属性一样使用它。
你希望利用缓存来避免不必要的重复计算,优化性能。
典型例子: 拼接全名、过滤列表、对数据进行格式化或排序。
使用侦听器的场景:
数据变化时,你需要执行异步操作(如 API 调用)。
数据变化时,你需要执行一个非幂等的、有副作用的操作(如操作 DOM、更改浏览器缓存、打印日志)。
你需要知道数据变化前后的具体值。
典型例子: 搜索框输入变化时请求搜索接口、表单验证、在 ID 变化时重新获取用户详情。
四、计算属性/侦听器 vs 方法
这是一个常见的困惑点:为什么不用一个方法来代替计算属性?
4.1 方法(Method)
方法是在被调用时才会执行的函数。它不具备响应性,也不会自动追踪依赖。
const firstName = ref('张');
const lastName = ref('三');// 一个方法
function getFullName() {console.log('方法 getFullName 被调用了!');return `${firstName.value} ${lastName.value}`;
}在模板中调用:<div>{{ getFullName() }}</div>
4.2 与方法的主要区别
| 特性 | 计算属性 / 侦听器 | 方法 (Method) |
|---|---|---|
| 响应性 | 是响应式的,自动依赖追踪 | 不是响应式的,只是一个普通函数 |
| 执行时机 | 计算属性:依赖变化时自动重新计算(有缓存)。 侦听器:依赖变化时自动执行。 | 只在被调用时执行 |
| 性能 | 计算属性有缓存,依赖未变时性能极佳 | 每次调用都会执行,无缓存 |
| 模板中的使用 | 计算属性在模板中作为属性使用:{{ fullName }} | 在模板中作为方法调用:{{ getFullName() }} |
4.3 示例对比:计算属性 vs 方法
假设我们在一个循环中多次使用这个值:
<!-- 使用计算属性 -->
<p>全名:{{ fullName }}</p>
<p>再次展示全名:{{ fullName }}</p>
<!-- 只要 firstName 或 lastName 不变,console 只会打印一次,第二次直接读取缓存 --><!-- 使用方法 -->
<p>全名:{{ getFullName() }}</p>
<p>再次展示全名:{{ getFullName() }}</p>
<!-- 每次渲染,两个 getFullName() 调用都会执行,console 会打印两次 -->从这个例子可以清晰地看到,对于衍生数据,计算属性因其缓存机制而具有显著的性能优势。
