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

各位靓仔靓女,晚上好!我是你们的老朋友,人称“Bug终结者”的阿飞。今晚我们来聊聊Vue 3 里两个非常重要的角色:watchwatchEffect

它们就像是Vue世界里的两只小精灵,专门负责监听数据的变化,并在数据改变时触发一些副作用,比如发起API请求、更新DOM、或者注册事件监听等等。

但别看它们个头小,用起来可是大有学问。今天咱们就来好好盘盘,如何玩转它们,特别是处理那些复杂的响应式副作用,让你的代码既优雅又高效。

第一幕:初识 watchwatchEffect

首先,让我们来简单认识一下这两位主角。

  • watch: 监听特定的响应式数据源,当数据源发生变化时,执行回调函数。你可以精确地控制监听哪些数据,以及如何响应变化。

  • watchEffect: 自动追踪回调函数中使用的所有响应式依赖,当这些依赖发生变化时,自动重新执行回调函数。它更像是一个“自动挡”,省去了手动指定依赖的麻烦。

简单来说,watch更精确,需要你告诉它“你要盯着谁”,而watchEffect更智能,它自己会看“你需要啥”。

第二幕:watch 的精细化控制

watch 就像一个训练有素的猎犬,你告诉它要追踪哪只兔子,它就会死死盯着,一旦兔子动了,它立刻汪汪叫。

<template>
  <div>
    <input v-model="searchText" placeholder="请输入搜索内容" />
    <button @click="fetchData">搜索</button>
    <ul>
      <li v-for="item in searchResults" :key="item.id">{{ item.name }}</li>
    </ul>
  </div>
</template>

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

const searchText = ref('');
const searchResults = ref([]);

const fetchData = async () => {
  // 模拟API请求
  await new Promise(resolve => setTimeout(resolve, 500));
  searchResults.value = Array.from({ length: 5 }, (_, i) => ({ id: i, name: `Result ${searchText.value}-${i}` }));
};

watch(searchText, async (newValue, oldValue) => {
  console.log(`searchText changed from ${oldValue} to ${newValue}`);
  if (newValue.length > 2) { // 至少输入3个字符才发起搜索
    await fetchData();
  } else {
    searchResults.value = []; // 清空搜索结果
  }
});
</script>

在这个例子中,我们使用 watch 监听 searchText 的变化。当 searchText 的值发生改变时,回调函数会被执行。回调函数接收两个参数:newValue(新值)和 oldValue(旧值)。

我们可以利用这两个参数来判断数据变化的具体情况,并进行相应的处理。比如,只有当 searchText 的长度大于 2 时,才发起 API 请求。

watch 的选项:更强大的控制力

watch 还提供了一些选项,可以进一步控制其行为。

选项 描述
immediate 是否在组件创建后立即执行回调函数。默认为 false
deep 是否深度监听对象内部属性的变化。默认为 false。如果监听的是一个对象,并且需要监听对象内部属性的变化,则需要设置为 true
flush 指定回调函数的执行时机。可以设置为 'pre'(在组件更新之前执行)或 'post'(在组件更新之后执行)。默认为 'pre'
onTrack 用于调试响应式依赖追踪。当一个响应式属性被作为依赖追踪时调用。该回调接收 debugger event 作为参数。
onTrigger 用于调试响应式依赖触发。当 watcher 的回调函数被响应式属性的变更触发时调用。该回调接收 debugger event 作为参数。
lazy (仅在使用 watch 函数时有效)创建一个惰性的 watcher,只有在手动调用 effect() 方法后才会开始监听。 这在某些高级场景中可能有用,例如你需要手动控制 watcher 何时开始监听依赖项。 通常情况下,你不需要使用此选项。

案例一:immediate 选项

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

const count = ref(0);

watch(count, (newValue) => {
  console.log(`Count is now: ${newValue}`);
}, { immediate: true }); // 组件创建后立即执行一次回调函数
</script>

在这个例子中,由于设置了 immediate: true,当组件创建时,回调函数会立即执行一次,输出 "Count is now: 0"。

案例二:deep 选项

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

const user = ref({
  name: 'John',
  age: 30,
  address: {
    city: 'New York'
  }
});

watch(user, (newValue) => {
  console.log('User changed:', newValue);
}, { deep: true });

// 修改user.address.city
user.value.address.city = 'Los Angeles'; // 触发 watch 回调
</script>

如果 deep 设置为 false (默认),修改 user.address.city 不会触发 watch 回调。只有当 user 对象本身被替换时才会触发。但设置 deep: true 后,修改 user 对象内部的任何属性都会触发 watch 回调。

案例三:flush 选项

flush 选项控制回调函数的执行时机。

  • 'pre' (默认): 在 Vue 组件更新 DOM 之前执行回调函数。
  • 'post': 在 Vue 组件更新 DOM 之后执行回调函数。
<template>
  <div>
    <p ref="messageRef">{{ message }}</p>
    <button @click="updateMessage">Update Message</button>
  </div>
</template>

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

const message = ref('Hello');
const messageRef = ref(null);

const updateMessage = () => {
  message.value = 'Updated Message';
  console.log('Message updated in component.');
};

watch(message, (newValue) => {
  console.log('Watch callback executed (pre-DOM update).');
  console.log('Current DOM content:', messageRef.value.textContent); // 仍然是旧值
}, { flush: 'pre' });

watch(message, async (newValue) => {
  await nextTick(); // 等待 DOM 更新
  console.log('Watch callback executed (post-DOM update).');
  console.log('Current DOM content:', messageRef.value.textContent); // 是新值
}, { flush: 'post' });
</script>

在这个例子中,第一个 watch 使用默认的 flush: 'pre',因此回调函数在 DOM 更新之前执行,所以 messageRef.value.textContent 仍然是旧值。第二个 watch 使用 flush: 'post',并且配合 nextTick 等待 DOM 更新,因此回调函数在 DOM 更新之后执行,messageRef.value.textContent 是新值。

第三幕:watchEffect 的自动追踪

watchEffect 就像一个勤劳的清洁工,它会自动扫描你的代码,找出所有用到的响应式数据,然后默默地监听它们的变化。只要其中任何一个数据发生了改变,它就会立即启动,执行回调函数。

<template>
  <div>
    <input v-model="name" placeholder="请输入姓名" />
    <input v-model="age" type="number" placeholder="请输入年龄" />
    <p>姓名:{{ name }},年龄:{{ age }}</p>
    <p>状态:{{ status }}</p>
  </div>
</template>

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

const name = ref('');
const age = ref(18);
const status = ref('');

watchEffect(() => {
  console.log('name or age changed');
  if (name.value && age.value >= 18) {
    status.value = '成年人';
  } else {
    status.value = '未成年人';
  }
});
</script>

在这个例子中,watchEffect 会自动追踪 nameage 的变化。只要 nameage 的值发生了改变,回调函数就会被执行,从而更新 status 的值。

watchEffect 的停止监听

watchEffect 会返回一个停止函数,你可以调用这个函数来停止监听。

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

const count = ref(0);
let stopWatchEffect = null;

onMounted(() => {
  stopWatchEffect = watchEffect(() => {
    console.log('Count:', count.value);
  });
});

onUnmounted(() => {
  if (stopWatchEffect) {
    stopWatchEffect(); // 组件卸载时停止监听
  }
});

// 模拟 count 值的变化
setInterval(() => {
  count.value++;
}, 1000);
</script>

在这个例子中,我们在组件挂载时使用 watchEffect 监听 count 的变化,并将返回的停止函数保存在 stopWatchEffect 变量中。在组件卸载时,我们调用 stopWatchEffect 函数来停止监听,防止内存泄漏。

watchEffect 的选项

watchEffect 也有一个 flush 选项,用于控制回调函数的执行时机,与 watchflush 选项相同。

第四幕:实战演练:处理复杂的响应式副作用

现在,让我们来看一些更复杂的场景,学习如何利用 watchwatchEffect 来处理响应式副作用。

场景一:根据用户输入动态加载数据

<template>
  <div>
    <input v-model="query" placeholder="请输入关键词" />
    <button @click="search">搜索</button>
    <ul>
      <li v-for="item in results" :key="item.id">{{ item.title }}</li>
    </ul>
  </div>
</template>

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

const query = ref('');
const results = ref([]);
const debouncedQuery = ref('');

// 使用 computed 实现防抖
const debouncedSearch = computed(() => {
  let timer = null;
  return (newQuery) => {
    clearTimeout(timer);
    return new Promise(resolve => {
      timer = setTimeout(() => {
        debouncedQuery.value = newQuery;
        resolve();
      }, 300);
    });
  };
});

const search = async () => {
    await debouncedSearch.value(query.value);
};

watch(debouncedQuery, async (newQuery) => {
  if (newQuery) {
    // 模拟 API 请求
    const data = await fetchData(newQuery);
    results.value = data;
  } else {
    results.value = [];
  }
});

const fetchData = async (query) => {
  // 模拟API请求
  await new Promise(resolve => setTimeout(resolve, 500));
  return Array.from({ length: 5 }, (_, i) => ({ id: i, title: `Result for ${query}-${i}` }));
};
</script>

在这个例子中,我们使用 watch 监听经过防抖处理的 debouncedQuery 的变化。当 debouncedQuery 的值发生改变时,发起 API 请求,并更新 results 的值。

场景二:监听多个数据的变化,执行复杂逻辑

<template>
  <div>
    <input v-model.number="price" type="number" placeholder="请输入商品价格" />
    <input v-model.number="quantity" type="number" placeholder="请输入商品数量" />
    <p>总价:{{ totalPrice }}</p>
  </div>
</template>

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

const price = ref(0);
const quantity = ref(0);

const totalPrice = computed(() => {
    return price.value * quantity.value;
});

</script>

在这个例子中,我们使用 computed 来计算总价,它会自动追踪 pricequantity 的变化。当 pricequantity 的值发生改变时,totalPrice 的值会自动更新。

场景三:监听路由变化,加载不同的数据

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

const route = useRoute();
const router = useRouter();
const data = ref(null);

watchEffect(async () => {
  const id = route.params.id;
  if (id) {
    // 模拟 API 请求
    data.value = await fetchData(id);
  } else {
      router.push('/');
  }
});

const fetchData = async (id) => {
  // 模拟API请求
  await new Promise(resolve => setTimeout(resolve, 500));
  return { id: id, name: `Item ${id}` };
};
</script>

在这个例子中,我们使用 watchEffect 监听路由参数 id 的变化。当 id 的值发生改变时,发起 API 请求,加载对应的数据。

第五幕:watch vs watchEffect:如何选择?

现在,我们已经了解了 watchwatchEffect 的基本用法和一些常见场景。那么,在实际开发中,我们应该如何选择呢?

特性 watch watchEffect
监听目标 需要明确指定要监听的数据源。可以是单个响应式变量、多个响应式变量组成的数组、一个 getter 函数,甚至是一个返回响应式数据的函数。 自动追踪回调函数中使用的所有响应式依赖。
灵活性 更加灵活。可以获取新值和旧值,可以设置 immediatedeepflush 等选项,可以精确控制监听行为。 相对简单。只能获取当前值,选项较少,控制力较弱。
适用场景 需要精确控制监听行为,或者需要获取新值和旧值,或者需要监听对象内部属性的变化时,使用 watch。例如,根据用户输入动态加载数据,监听对象内部属性的变化,等等。 只需要根据响应式数据的变化执行副作用,不需要精确控制监听行为时,使用 watchEffect。例如,根据多个响应式数据的变化更新状态,监听路由变化加载数据,等等。
调试 调试起来可能稍微麻烦一些,需要手动检查监听目标是否正确,以及回调函数是否正确执行。 调试起来相对简单,只需要检查回调函数中使用的响应式数据是否正确,以及回调函数是否正确执行。
性能 通常情况下,watch 的性能会更好一些,因为它只需要监听指定的数据源,而 watchEffect 需要自动追踪所有依赖。但是,在某些情况下,如果 watchEffect 追踪的依赖较少,或者 watch 需要监听多个数据源,那么 watchEffect 的性能可能会更好。 通常情况下,watchEffect 的性能会稍微差一些,因为它需要自动追踪所有依赖。但是,在某些情况下,如果 watchEffect 追踪的依赖较少,或者 watch 需要监听多个数据源,那么 watchEffect 的性能可能会更好。
代码可读性 如果监听目标比较复杂,或者需要设置多个选项,那么 watch 的代码可能会比较冗长。 watchEffect 的代码通常会更简洁,因为它不需要手动指定监听目标。

总结一下:

  • 如果你需要 精确控制 监听行为,比如需要知道数据的旧值和新值,或者需要深度监听对象,那就选择 watch
  • 如果你只是想 简单地响应 某些数据的变化,而不需要关心具体的变化细节,那就选择 watchEffect

第六幕:避坑指南

在使用 watchwatchEffect 时,有一些常见的坑需要注意:

  1. 内存泄漏:如果在组件卸载时没有停止监听,可能会导致内存泄漏。记得在 onUnmounted 钩子函数中调用停止函数。
  2. 无限循环:如果在回调函数中修改了监听的数据源,可能会导致无限循环。要避免这种情况,可以使用 nextTick 函数,或者使用 computed 属性。
  3. 不必要的监听:不要监听不必要的数据,这会降低性能。只监听真正需要监听的数据。
  4. 滥用 deep 选项deep 选项会深度监听对象内部属性的变化,这会带来性能开销。只有在真正需要深度监听时才使用 deep 选项。

第七幕:总结与展望

今天,我们一起学习了 Vue 3 中 watchwatchEffect 的基本用法、选项、以及一些常见场景。希望通过今天的学习,你能更好地掌握它们,并在实际开发中灵活运用。

watchwatchEffect 是 Vue 中非常重要的 API,掌握它们可以帮助你更好地处理响应式副作用,编写出更优雅、更高效的代码。

好了,今天的分享就到这里。希望大家有所收获!下次再见!

发表回复

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