在 Vue3 中,watchEffect 是一个用于在响应式数据变化时自动重新执行的函数。它在创建响应式副作用时特别有用,比如在某些数据变化时更新 DOM、发起网络请求或处理复杂的逻辑。
watchEffect 的 onInvalidate 是一个非常重要的功能,用于处理副作用函数,理解它可以帮助我们在复杂的应用中更好地管理资源,避免内存泄漏和不必要的副作用执行。
1. watchEffect 基本使用
watchEffect 用于自动追踪其内部副作用函数中的响应式数据,当这些数据发生变化时,副作用函数会重新执行。
举个 🌰
import { ref, watchEffect } from 'vue';
const count = ref(0);
watchEffect(() => {
console.log(`Count is: ${count.value}`);
});
2. onInvalidate 基本使用
onInvalidate 是一个参数,用于处理副作用的取消或清理操作。当在副作用函数中使用了某些资源(如定时器、订阅等),可能需要在副作用函数重新执行之前清理这些资源,以防止内存泄漏或其他问题。
举个 🌰
<template>
<div>
<p>当前计数: {{ count }}</p>
<button @click="count++">增加计数</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect((onInvalidate) => {
console.log('副作用函数执行');
const timer = setInterval(() => {
console.log(`计时器中 count 的值: ${count.value}`);
}, 1000);
onInvalidate(() => {
console.log('清除计时器');
clearInterval(timer);
});
});
return {
count,
};
},
};
</script>
按道理而言:使用 onInvalidate 清理计时器,每次 count 变化时,watchEffect 会重新执行,在此之前 onInvalidate 会先清理掉之前的计时器,避免重复创建计时器导致内存泄漏。
当我们点击按钮改变 count 值时,watchEffect 重新执行,每秒打印出值,但是 onInvalidate 失效,console.log 没有输出“清楚计时器”,大家可以先分析一下为什么哦?
原因:
watchEffect 的 onInvalidate 在依赖项发生变化时会被触发,关键在于依赖的正确追踪。而上面代码中,count 的变化没有触发 onInvalidate,是因为 watchEffect 没有正确追踪到 count 的依赖。
watchEffect 只能追踪在副作用函数内部使用的响应式数据。如果在 watchEffect 没有直接使用 count.value ,那么它的变化就不会触发副作用函数重新执行,从而不会调用 onInvalidate 清理之前的计时器。
虽然在 setInterval 的回调函数中使用了 count.value,但这并不足以让 watchEffect 追踪到 count 的变化。watchEffect 只会在它同步执行的那一刻,追踪它直接访问的响应式数据。如果响应式数据只是在异步回调(如 setInterval)中访问,它将不会触发 watchEffect 的重新执行。
解决方案:
在 watchEffect 的主逻辑中,直接访问 count.value 即可。
<template>
<div>
<p>当前计数: {{ count }}</p>
<button @click="count++">增加计数</button>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const count = ref(0);
watchEffect((onInvalidate) => {
// 在函数内直接读取 count.value,确保它被追踪,这一步很重要!!!
console.log(`副作用函数执行,count 值为: ${count.value}`);
const timer = setInterval(() => {
console.log(`计时器中 count 的值: ${count.value}`);
}, 1000);
onInvalidate(() => {
console.log('清除计时器 timer');
clearInterval(timer);
});
});
return {
count,
};
},
};
</script>
此时控制台输出的结果就是我们预期的!
3. 引入原因
1、清理副作用
在副作用函数中使用 onInvalidate 可以确保在副作用函数重新执行之前,之前的副作用会被正确清理。这样可以避免资源泄漏,例如未清除的定时器或未取消的事件监听器。
2、处理异步操作
对于异步操作(如 API 请求),onInvalidate 可以用于取消未完成的请求。例如,在某个数据变化时,取消之前发出的请求。
3、避免性能问题
在副作用函数中进行清理操作可以避免重复的资源消耗,提升应用性能。例如,在组件卸载时清理定时器或订阅可以避免不必要的计算和内存使用。
4. 注意事项
1、副作用函数的重用
onInvalidate 只在副作用函数重新执行之前调用。如果副作用函数的逻辑是复用的,确保清理操作不会在副作用函数的每次执行中重复触发。
2、异步操作的取消
对于异步操作,确保在 onInvalidate 中正确地取消操作。如果使用 fetch 或其他异步 API,考虑使用 AbortController 进行取消。
3、不必要的清理
避免在 onInvalidate 中做过多的清理操作,确保只处理真正需要的清理逻辑。
举个 🌰
假设有一个网络请求场景,每当数据变化时都会重新发起网络请求,但如果请求未完成时数据再次变化,则会取消之前的请求。在这里,我们只希望 onInvalidate 在新的请求发起之前调用,而不是每次请求逻辑执行时都重复触发清理操作。
<template>
<div>
<p>当前ID: {{ id }}</p>
<button @click="id++">增加 ID</button>
<p>结果: {{ result }}</p>
</div>
</template>
<script>
import { ref, watchEffect } from 'vue';
export default {
setup() {
const id = ref(1);
const result = ref('');
watchEffect((onInvalidate) => {
console.log(`发起新的请求,ID 为: ${id.value}`);
// 模拟一个网络请求
const controller = new AbortController();
fetch(`https://xxx.xxx.com/posts/${id.value}`, { signal: controller.signal })
.then((response) => response.json())
.then((data) => {
console.log('请求成功:', data);
result.value = data.title;
})
.catch((error) => {
if (error.name === 'AbortError') {
console.log('请求被取消');
} else {
console.error('请求失败:', error);
}
});
// 只在发起新请求之前,取消前一次的请求
onInvalidate(() => {
console.log('清理函数执行,取消之前的请求');
controller.abort();
});
});
return {
id,
result,
};
},
};
</script>
初次加载时:
正常点击展示:
当按钮点击过快时:
总结
onInvalidate 是 watchEffect 中用于清理副作用函数的有力工具。它可以帮助开发者在副作用函数重新执行之前清理掉不需要的资源或取消正在进行的操作,从而提高应用的性能和稳定性。