摘要: 在 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 会打印两次 -->

从这个例子可以清晰地看到,对于衍生数据,计算属性因其缓存机制而具有显著的性能优势。

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐