各位靓仔靓女,晚上好!我是你们的老朋友,人称“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,掌握它们可以帮助你更好地处理响应式副作用,编写出更优雅、更高效的代码。
好了,今天的分享就到这里。希望大家有所收获!下次再见!