Vue 3 响应式副作用:watch
和 watchEffect
高级用法讲座
大家好啊!我是你们今天的讲师,很高兴能和大家一起聊聊 Vue 3 中 watch
和 watchEffect
这两个“响应式副作用”神器。听起来有点高大上,但其实它们的作用很简单,就是帮你处理数据变化后需要执行的各种“后事”。就像你家水龙头(响应式数据)拧开了,你得用水洗手、洗菜、浇花(副作用)一样。
今天咱们就深入探讨一下,如何利用它们处理复杂的场景,比如 API 请求、事件监听等等。准备好了吗? Let’s dive in!
watch
vs watchEffect
:傻傻分不清楚?
首先,我们来聊聊 watch
和 watchEffect
的区别。很多人刚接触 Vue 3 的时候都会觉得它们很像,但其实它们的设计理念和使用场景有很大的不同。
watch
: 更像是“侦探”,你需要明确告诉它你要监视哪个“嫌疑人”(响应式数据),然后它才会开始工作。你可以指定监视多个“嫌疑人”,也可以选择深度监视。watchEffect
: 更像是“间谍”,它会自动“窃听”你的代码,找出所有用到的响应式数据,并建立依赖关系。只要这些数据发生变化,它就会立即执行。
用一个表格来概括一下:
特性 | watch |
watchEffect |
---|---|---|
依赖追踪 | 需要显式指定要监听的响应式数据。 | 自动追踪在回调函数中使用的所有响应式数据。 |
执行时机 | 只有当被监听的响应式数据发生变化时才会执行回调函数。 | 首次渲染时会立即执行一次,之后只要依赖的响应式数据发生变化就会执行。 |
控制权 | 拥有更多的控制权,可以访问新值和旧值,可以进行条件判断,决定是否执行副作用。 | 控制权较少,主要用于执行简单的副作用,无法直接访问新值和旧值(可以通过其他方式间接实现)。 |
适用场景 | 需要对特定数据的变化做出响应,例如根据用户输入过滤数据、根据路由变化加载数据等。 | 需要在组件渲染后立即执行副作用,例如初始化第三方库、监听全局事件等。 |
懒加载 | 默认是懒加载的,只有当被监听的响应式数据被访问时才会开始监听。 | 默认是立即执行的。 |
返回值 | 返回一个 stop 函数,用于停止监听。 |
返回一个 stop 函数,用于停止执行副作用。 |
实战演练:API 请求
API 请求是前端开发中最常见的副作用之一。 让我们看看如何使用 watch
和 watchEffect
来处理 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-es
的 debounce
函数对 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 请求,事件监听也是前端开发中常见的副作用。 让我们看看如何使用 watch
和 watchEffect
来处理事件监听。
场景一:监听窗口大小变化
假设我们需要在窗口大小发生变化时,更新组件的布局。 使用 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
函数,用于更新 width
和 height
的值。 然后,我们使用 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
。
高级技巧:watch
的 flush
选项
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 中 watch
和 watchEffect
的高级用法。 我们学习了如何使用它们处理 API 请求、事件监听等复杂的响应式副作用。 我们还学习了如何使用防抖优化 API 请求,以及如何使用 watch
的 flush
选项控制回调函数的执行时机。
希望今天的讲座能帮助大家更好地理解和使用 watch
和 watchEffect
。 记住,选择哪个取决于你的具体需求! watch
适合精确控制,watchEffect
适合自动追踪。
多练习,多思考,你也能成为响应式副作用的大师! 下次再见!