在 Vue 3 应用中,如何利用 `watch` 和 `watchEffect`,处理复杂的响应式副作用,例如 API 请求和事件监听?

Vue 3 响应式副作用:watchwatchEffect 高级用法讲座

大家好啊!我是你们今天的讲师,很高兴能和大家一起聊聊 Vue 3 中 watchwatchEffect 这两个“响应式副作用”神器。听起来有点高大上,但其实它们的作用很简单,就是帮你处理数据变化后需要执行的各种“后事”。就像你家水龙头(响应式数据)拧开了,你得用水洗手、洗菜、浇花(副作用)一样。

今天咱们就深入探讨一下,如何利用它们处理复杂的场景,比如 API 请求、事件监听等等。准备好了吗? Let’s dive in!

watch vs watchEffect:傻傻分不清楚?

首先,我们来聊聊 watchwatchEffect 的区别。很多人刚接触 Vue 3 的时候都会觉得它们很像,但其实它们的设计理念和使用场景有很大的不同。

  • watch: 更像是“侦探”,你需要明确告诉它你要监视哪个“嫌疑人”(响应式数据),然后它才会开始工作。你可以指定监视多个“嫌疑人”,也可以选择深度监视。
  • watchEffect: 更像是“间谍”,它会自动“窃听”你的代码,找出所有用到的响应式数据,并建立依赖关系。只要这些数据发生变化,它就会立即执行。

用一个表格来概括一下:

特性 watch watchEffect
依赖追踪 需要显式指定要监听的响应式数据。 自动追踪在回调函数中使用的所有响应式数据。
执行时机 只有当被监听的响应式数据发生变化时才会执行回调函数。 首次渲染时会立即执行一次,之后只要依赖的响应式数据发生变化就会执行。
控制权 拥有更多的控制权,可以访问新值和旧值,可以进行条件判断,决定是否执行副作用。 控制权较少,主要用于执行简单的副作用,无法直接访问新值和旧值(可以通过其他方式间接实现)。
适用场景 需要对特定数据的变化做出响应,例如根据用户输入过滤数据、根据路由变化加载数据等。 需要在组件渲染后立即执行副作用,例如初始化第三方库、监听全局事件等。
懒加载 默认是懒加载的,只有当被监听的响应式数据被访问时才会开始监听。 默认是立即执行的。
返回值 返回一个 stop 函数,用于停止监听。 返回一个 stop 函数,用于停止执行副作用。

实战演练:API 请求

API 请求是前端开发中最常见的副作用之一。 让我们看看如何使用 watchwatchEffect 来处理 API 请求。

场景一:根据用户输入搜索商品

假设我们有一个搜索框,用户输入关键词后,我们需要调用 API 获取相关的商品列表。 使用 watch 实现:

<template>
  <input v-model="keyword" type="text">
  <ul>
    <li v-for="item in products" :key="item.id">{{ item.name }}</li>
  </ul>
</template>

<script setup>
import { ref, watch } from 'vue';

const keyword = ref('');
const products = ref([]);

const fetchProducts = async (keyword) => {
  // 模拟 API 请求
  await new Promise(resolve => setTimeout(resolve, 500));
  const data = Array.from({ length: 5 }, (_, i) => ({ id: i, name: `Product ${keyword}-${i}` }));
  return data;
};

watch(keyword, async (newKeyword, oldKeyword) => {
  console.log(`Keyword changed from ${oldKeyword} to ${newKeyword}`);
  if (newKeyword) {
    products.value = await fetchProducts(newKeyword);
  } else {
    products.value = []; // 清空商品列表
  }
}, { immediate: false }); // 默认不立即执行,等 keyword 变化再执行
</script>

在这个例子中,我们使用 watch 监听 keyword 的变化。 当 keyword 发生变化时,watch 的回调函数会被执行。 在回调函数中,我们调用 fetchProducts 函数获取商品列表,并将结果赋值给 products

优化:防抖 (Debounce)

如果用户输入速度很快,每次输入都触发 API 请求,会对服务器造成很大的压力。 我们可以使用防抖来优化这个逻辑。

import { ref, watch } from 'vue';
import { debounce } from 'lodash-es'; // 引入 lodash-es 的 debounce 函数

const keyword = ref('');
const products = ref([]);

const fetchProducts = async (keyword) => {
  // 模拟 API 请求
  await new Promise(resolve => setTimeout(resolve, 500));
  const data = Array.from({ length: 5 }, (_, i) => ({ id: i, name: `Product ${keyword}-${i}` }));
  return data;
};

const debouncedFetchProducts = debounce(async (keyword) => {
  products.value = await fetchProducts(keyword);
}, 300); // 300ms 的防抖时间

watch(keyword, (newKeyword) => {
  if (newKeyword) {
    debouncedFetchProducts(newKeyword);
  } else {
    products.value = [];
  }
});

这里我们使用了 lodash-esdebounce 函数对 fetchProducts 函数进行防抖处理。 这样,只有当用户停止输入 300ms 后,才会触发 API 请求。

场景二:根据路由变化加载数据

假设我们有一个商品详情页,需要根据路由参数 id 加载商品信息。 使用 watchEffect 实现:

<template>
  <h1>{{ product.name }}</h1>
  <p>{{ product.description }}</p>
</template>

<script setup>
import { ref, watchEffect } from 'vue';
import { useRoute } from 'vue-router';

const route = useRoute();
const product = ref({});

const fetchProduct = async (id) => {
  // 模拟 API 请求
  await new Promise(resolve => setTimeout(resolve, 500));
  return { id: id, name: `Product ${id}`, description: `This is product ${id}` };
};

watchEffect(async () => {
  const id = route.params.id;
  console.log(`Route changed, loading product with id: ${id}`);
  product.value = await fetchProduct(id);
});
</script>

在这个例子中,我们使用 watchEffect 监听 route.params.id 的变化。 当 route.params.id 发生变化时,watchEffect 的回调函数会被执行。 在回调函数中,我们调用 fetchProduct 函数获取商品信息,并将结果赋值给 product

watchEffect 的好处:

  • 代码更简洁: 不需要显式指定要监听的响应式数据,watchEffect 会自动追踪。
  • 更灵活: 可以监听多个响应式数据,只要其中任何一个发生变化,回调函数就会被执行。

watchEffect 的缺点:

  • 首次渲染时会立即执行: 这可能会导致不必要的 API 请求。
  • 无法访问新值和旧值: 这可能会使某些逻辑难以实现。

实战演练:事件监听

除了 API 请求,事件监听也是前端开发中常见的副作用。 让我们看看如何使用 watchwatchEffect 来处理事件监听。

场景一:监听窗口大小变化

假设我们需要在窗口大小发生变化时,更新组件的布局。 使用 watchEffect 实现:

<template>
  <div>
    Width: {{ width }}, Height: {{ height }}
  </div>
</template>

<script setup>
import { ref, watchEffect } from 'vue';

const width = ref(window.innerWidth);
const height = ref(window.innerHeight);

watchEffect(() => {
  const handleResize = () => {
    width.value = window.innerWidth;
    height.value = window.innerHeight;
  };

  window.addEventListener('resize', handleResize);

  // 返回一个清理函数,在组件卸载或依赖变化时移除事件监听
  return () => {
    window.removeEventListener('resize', handleResize);
  };
});
</script>

在这个例子中,我们使用 watchEffect 监听窗口大小的变化。 在 watchEffect 的回调函数中,我们定义了一个 handleResize 函数,用于更新 widthheight 的值。 然后,我们使用 window.addEventListener 函数监听 resize 事件,并将 handleResize 函数作为回调函数。

注意: watchEffect 的回调函数可以返回一个清理函数。 这个清理函数会在组件卸载或依赖变化时被执行。 在清理函数中,我们需要移除事件监听,以避免内存泄漏。

场景二:监听自定义事件

假设我们有一个子组件,会触发一个自定义事件 update:modelValue。 我们需要在父组件中监听这个事件,并更新父组件的状态。 使用 watch 实现:

// ChildComponent.vue
<template>
  <input :value="modelValue" @input="$emit('update:modelValue', $event.target.value)">
</template>

<script setup>
import { defineProps, defineEmits } from 'vue';

defineProps({
  modelValue: String
});

defineEmits(['update:modelValue']);
</script>

// ParentComponent.vue
<template>
  <ChildComponent :modelValue="message" @update:modelValue="updateMessage" />
  <p>Message: {{ message }}</p>
</template>

<script setup>
import { ref, watch } from 'vue';
import ChildComponent from './ChildComponent.vue';

const message = ref('');

const updateMessage = (newValue) => {
  message.value = newValue;
};

// 不需要 watch 了,因为 @update:modelValue 直接更新了 message
// watch(message, (newValue, oldValue) => {
//   console.log(`Message changed from ${oldValue} to ${newValue}`);
// });
</script>

在这个例子中,子组件通过 $emit('update:modelValue', $event.target.value) 触发了一个自定义事件。 父组件通过 @update:modelValue="updateMessage" 监听了这个事件,并将 updateMessage 函数作为回调函数。 在 updateMessage 函数中,我们更新了 message 的值。 这里不需要 watch 了,因为事件已经直接更新了 message。 如果需要做一些额外的事情,比如记录日志,那么可以加上 watch

高级技巧:watchflush 选项

watch 提供了一个 flush 选项,用于控制回调函数的执行时机。 flush 选项可以设置为以下三个值:

  • 'pre':在组件更新之前执行回调函数。
  • 'post':在组件更新之后执行回调函数(默认值)。
  • 'sync':同步执行回调函数。

通常情况下,我们不需要修改 flush 选项。 但是,在某些特殊情况下,修改 flush 选项可以解决一些问题。

场景:在组件更新之前获取 DOM 元素

假设我们需要在组件更新之前获取 DOM 元素的高度。 如果使用默认的 flush 选项,获取到的高度可能是不正确的,因为 DOM 元素还没有更新。 我们可以将 flush 选项设置为 'pre',以确保在组件更新之前获取 DOM 元素的高度。

<template>
  <div ref="myDiv">
    Content
  </div>
</template>

<script setup>
import { ref, watch, onMounted } from 'vue';

const myDiv = ref(null);
const divHeight = ref(0);

onMounted(() => {
  watch(myDiv, (newDiv) => {
    if (newDiv) {
      divHeight.value = newDiv.offsetHeight;
      console.log(`Div height: ${divHeight.value}`);
    }
  }, { flush: 'pre' }); // 组件更新前执行
});
</script>

在这个例子中,我们使用 watch 监听 myDiv 的变化。 当 myDiv 发生变化时,watch 的回调函数会被执行。 在回调函数中,我们获取 myDiv 的高度,并将结果赋值给 divHeight。 我们将 flush 选项设置为 'pre',以确保在组件更新之前获取 myDiv 的高度。

总结

今天我们深入探讨了 Vue 3 中 watchwatchEffect 的高级用法。 我们学习了如何使用它们处理 API 请求、事件监听等复杂的响应式副作用。 我们还学习了如何使用防抖优化 API 请求,以及如何使用 watchflush 选项控制回调函数的执行时机。

希望今天的讲座能帮助大家更好地理解和使用 watchwatchEffect。 记住,选择哪个取决于你的具体需求! watch 适合精确控制,watchEffect 适合自动追踪。

多练习,多思考,你也能成为响应式副作用的大师! 下次再见!

发表回复

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