Vue 3的`computed`与`watch`:如何在不同场景下进行选择?

Vue 3 的 Computed 与 Watch:不同场景下的选择之道

大家好,今天我们来深入探讨 Vue 3 中两个非常重要的特性:computedwatch。它们都用于响应式数据的变化,但适用场景却有所不同。理解它们的区别,并根据具体需求选择合适的方案,是编写高效、可维护的 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 是一个计算属性,它依赖于 pricediscount。当 pricediscount 的值发生变化时,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 的值来更新 firstNamelastName

何时使用 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 监听了 firstNamelastName 的变化,并在其中任何一个值发生变化时,输出相应的日志。注意,回调函数接收的 newValuesoldValues 是一个数组,分别对应监听的数据的新值和旧值。

深度监听:

<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

一些具体的例子:

  1. 显示用户全名: computed 是更好的选择,因为它只是简单地将 firstNamelastName 拼接起来。

    const fullName = computed(() => firstName.value + ' ' + lastName.value);
  2. 根据用户输入搜索数据: computed 可以用于过滤数据,但 watch 更适合处理搜索请求,因为它可能涉及到异步操作,例如发送网络请求。

    // 使用 computed 过滤数据
    const filteredData = computed(() => {
      return data.value.filter(item => item.name.includes(searchTerm.value));
    });
    
    // 使用 watch 发送搜索请求
    watch(searchTerm, (newValue) => {
      // 发送网络请求,获取搜索结果
      fetchData(newValue);
    });
  3. 在数据变化时更新第三方图表: watch 是更好的选择,因为它需要执行副作用操作(更新图表)。

    watch(data, (newValue) => {
      // 调用第三方图表库的 API,更新图表
      updateChart(newValue);
    });
  4. 在用户登录成功后跳转到首页: watch 是更好的选择,因为它需要监听用户登录状态的变化,并执行跳转操作。

    watch(isLoggedIn, (newValue) => {
      if (newValue) {
        router.push('/');
      }
    });

最佳实践

  1. 尽量使用 computed 来处理数据转换和派生状态,避免在模板中编写复杂的表达式。
  2. 避免在 watch 的回调函数中执行大量的计算操作,尽量将计算逻辑放到 computed 中。
  3. 使用 deep: true 选项时要谨慎,因为它会带来性能开销。尽量只在必要时才开启深度监听。
  4. 在组件销毁时,取消 watch 的监听,避免内存泄漏。(Vue 3会自动处理,不需要手动取消)
  5. 合理使用 immediate: true 选项,避免不必要的初始执行。
  6. 如果需要监听多个数据的变化,尽量将它们放到一个数组中,然后使用 watch 监听这个数组。
  7. 避免在 computed 的 setter 中修改其他响应式数据,这可能会导致循环依赖。
  8. 尽量保持 computedwatch 的回调函数简洁明了,避免包含过多的业务逻辑。
  9. 使用 watch 时,注意处理竞态条件,例如在发送网络请求时,取消之前的请求。

性能考量

computed 的缓存机制使其在性能方面通常优于 watch,尤其是在计算量大的情况下。 然而,watch 提供了更大的灵活性,在某些场景下是不可替代的。

  • Computed: 因为有缓存,所以除非依赖发生改变,否则不会重新计算。这意味着对于相同的输入,computed 只会计算一次。
  • Watch: 每次监听的数据发生改变,都会执行回调函数。如果监听的数据频繁变化,watch 的性能开销会比较大。特别是使用了 deep: true,会进行深度比较,开销更大。

因此,在选择 computedwatch 时,需要权衡性能和灵活性。如果性能是关键因素,并且可以使用 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 的值。

总结

computedwatch 是 Vue 3 中强大的工具,用于响应式数据的变化。 理解它们的区别和适用场景,并根据具体需求选择合适的方案,可以编写出高效、可维护的 Vue 应用。

快速回顾与应用建议

总结一下,computed 适合数据转换和派生,watch 适合副作用操作。选择时要考虑性能和灵活性,并根据具体场景进行权衡。希望今天的讲解能够帮助大家更好地理解和使用 computedwatch

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注