各位靓仔靓女,晚上好!我是你们的老朋友,人称“Bug终结者”的阿飞。今晚我们来聊聊Vue 3 里两个非常重要的角色:watch 和 watchEffect。
它们就像是Vue世界里的两只小精灵,专门负责监听数据的变化,并在数据改变时触发一些副作用,比如发起API请求、更新DOM、或者注册事件监听等等。
但别看它们个头小,用起来可是大有学问。今天咱们就来好好盘盘,如何玩转它们,特别是处理那些复杂的响应式副作用,让你的代码既优雅又高效。
第一幕:初识 watch 和 watchEffect
首先,让我们来简单认识一下这两位主角。
-
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 会自动追踪 name 和 age 的变化。只要 name 或 age 的值发生了改变,回调函数就会被执行,从而更新 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 选项,用于控制回调函数的执行时机,与 watch 的 flush 选项相同。
第四幕:实战演练:处理复杂的响应式副作用
现在,让我们来看一些更复杂的场景,学习如何利用 watch 和 watchEffect 来处理响应式副作用。
场景一:根据用户输入动态加载数据
<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 来计算总价,它会自动追踪 price 和 quantity 的变化。当 price 或 quantity 的值发生改变时,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:如何选择?
现在,我们已经了解了 watch 和 watchEffect 的基本用法和一些常见场景。那么,在实际开发中,我们应该如何选择呢?
| 特性 | watch |
watchEffect |
|---|---|---|
| 监听目标 | 需要明确指定要监听的数据源。可以是单个响应式变量、多个响应式变量组成的数组、一个 getter 函数,甚至是一个返回响应式数据的函数。 | 自动追踪回调函数中使用的所有响应式依赖。 |
| 灵活性 | 更加灵活。可以获取新值和旧值,可以设置 immediate、deep、flush 等选项,可以精确控制监听行为。 |
相对简单。只能获取当前值,选项较少,控制力较弱。 |
| 适用场景 | 需要精确控制监听行为,或者需要获取新值和旧值,或者需要监听对象内部属性的变化时,使用 watch。例如,根据用户输入动态加载数据,监听对象内部属性的变化,等等。 |
只需要根据响应式数据的变化执行副作用,不需要精确控制监听行为时,使用 watchEffect。例如,根据多个响应式数据的变化更新状态,监听路由变化加载数据,等等。 |
| 调试 | 调试起来可能稍微麻烦一些,需要手动检查监听目标是否正确,以及回调函数是否正确执行。 | 调试起来相对简单,只需要检查回调函数中使用的响应式数据是否正确,以及回调函数是否正确执行。 |
| 性能 | 通常情况下,watch 的性能会更好一些,因为它只需要监听指定的数据源,而 watchEffect 需要自动追踪所有依赖。但是,在某些情况下,如果 watchEffect 追踪的依赖较少,或者 watch 需要监听多个数据源,那么 watchEffect 的性能可能会更好。 |
通常情况下,watchEffect 的性能会稍微差一些,因为它需要自动追踪所有依赖。但是,在某些情况下,如果 watchEffect 追踪的依赖较少,或者 watch 需要监听多个数据源,那么 watchEffect 的性能可能会更好。 |
| 代码可读性 | 如果监听目标比较复杂,或者需要设置多个选项,那么 watch 的代码可能会比较冗长。 |
watchEffect 的代码通常会更简洁,因为它不需要手动指定监听目标。 |
总结一下:
- 如果你需要 精确控制 监听行为,比如需要知道数据的旧值和新值,或者需要深度监听对象,那就选择
watch。 - 如果你只是想 简单地响应 某些数据的变化,而不需要关心具体的变化细节,那就选择
watchEffect。
第六幕:避坑指南
在使用 watch 和 watchEffect 时,有一些常见的坑需要注意:
- 内存泄漏:如果在组件卸载时没有停止监听,可能会导致内存泄漏。记得在
onUnmounted钩子函数中调用停止函数。 - 无限循环:如果在回调函数中修改了监听的数据源,可能会导致无限循环。要避免这种情况,可以使用
nextTick函数,或者使用computed属性。 - 不必要的监听:不要监听不必要的数据,这会降低性能。只监听真正需要监听的数据。
- 滥用
deep选项:deep选项会深度监听对象内部属性的变化,这会带来性能开销。只有在真正需要深度监听时才使用deep选项。
第七幕:总结与展望
今天,我们一起学习了 Vue 3 中 watch 和 watchEffect 的基本用法、选项、以及一些常见场景。希望通过今天的学习,你能更好地掌握它们,并在实际开发中灵活运用。
watch 和 watchEffect 是 Vue 中非常重要的 API,掌握它们可以帮助你更好地处理响应式副作用,编写出更优雅、更高效的代码。
好了,今天的分享就到这里。希望大家有所收获!下次再见!