清理 React useEffect Hook 中的异步函数(取消订阅)
React 中的函数组件之所以如此精美,是因为React Hooks的存在。借助 Hooks,我们可以更改状态、在组件挂载和卸载时执行操作等等。
虽然所有这些都很美好,但在使用 useEffect hook 时,有一个小警告(或者可能不是)会有点令人沮丧。
在我们研究这个问题之前,让我们快速回顾一下 useEffect 钩子。
效果钩
useEffect 钩子允许您在组件安装和卸载时执行操作。
useEffect(() => {
// actions performed when component mounts
return () => {
// actions to be performed when component unmounts
}
}, []);
useEffect
根据函数的第二个参数来调用函数的回调函数useEffect
。
第二个参数是依赖项数组。你可以在这里列出你的依赖项。
因此,只要任何依赖项有更新,就会调用回调函数。
useEffect(() => {
if (loading) {
setUsername('Stranger');
}
}, [loading]);
如果依赖项数组为空(就像我们的第一个示例一样),则 React 将只调用该函数一次,即在组件安装时。
但是你可能会想,“当它卸载时,React 不也会调用该函数吗?”。
嗯,不。返回的函数是一个闭包,当你在需要的函数(现在的返回函数)中访问父函数的作用域时,你实际上不需要调用父函数(现在是回调函数)。
如果您对此还不清楚,请花 7 分钟时间阅读我撰写的有关JavaScript 闭包的文章。
现在我们已经回顾了基础知识,让我们来看看异步函数的问题。
React 中的异步函数
毫无疑问,你可能曾经在 useEffect hook 中使用过异步函数。如果你还没有用过,那么你很快就会用到它。
但是,当我们在 useEffect hook 中使用异步函数时,卸载并挂载组件时,React 会发出一个警告。这是警告
如果你看不到图片,这里是警告
Can't perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
指令非常清晰明了:“在 useEffect 的清理函数中取消所有订阅和异步任务”。好吧,我明白你的 React 心思了!但是我该怎么做呢?
很简单,非常简单。React 抛出这个警告的原因是因为我在 async 函数中使用了 setState。
这不算什么罪过。但 React 会在组件卸载后尝试更新状态,这算是一种罪过(泄漏罪)。
这是导致上述警告的代码
useEffect(() => {
setTimeout(() => {
setUsername('hello world');
}, 4000);
}, []);
我们该如何解决这个问题?我们只需告诉 React 仅在挂载时尝试更新异步函数中的任何状态。
因此我们有
useEffect(() => {
let mounted = true;
setTimeout(() => {
if (mounted) {
setUsername('hello world');
}
}, 4000);
}, []);
好的,现在我们取得了一些进展。目前我们只是告诉 React,如果mounted
(你可以称之为已订阅或其他)为 true,则执行更新。
但是该mounted
变量始终为 true,因此无法防止警告或应用程序泄漏。那么,我们该如何以及何时将其设置为 false?
当组件卸载时,我们可以也应该将其设置为 false。所以我们现在有
useEffect(() => {
let mounted = true;
setTimeout(() => {
if (mounted) {
setUsername('hello world');
}
}, 4000);
return () => mounted = false;
}, []);
因此,当组件卸载时,mounted
变量将更改为 false,因此setUsername
在组件卸载时该函数将不会更新。
我们可以通过看到的第一段代码来判断组件何时挂载和卸载。
useEffect(() => {
// actions performed when component mounts
return () => {
// actions to be performed when component unmounts
}
}, []);
这是取消订阅异步函数的方法,您可以通过不同的方式执行此操作,例如
useEffect(() => {
let t = setTimeout(() => {
setUsername('hello world');
}, 4000);
return () => clearTimeout(t);
}, []);
这是一个带有 API 的异步函数的示例fetch
。
useEffect(() => {
let mounted = true;
(async () => {
const res = await fetch('example.com');
if (mounted) {
// only try to update if we are subscribed (or mounted)
setUsername(res.username);
}
})();
return () => mounted = false; // cleanup function
}, []);
更新:正如@joeattardi在评论中所建议的,我们可以使用该AbortController
界面来中止Fetch
请求,而不仅仅是在卸载时阻止更新。
这是最后一个例子的重构代码。
useEffect(() => {
const controller = new AbortController();
const signal = controller.signal;
(async () => {
const res = await fetch('example.com', {
signal,
});
setUsername(res.username));
})();
return () => controller.abort();
}, []);
现在 React 将不会尝试更新该setUsername
函数,因为请求已被中止。就像重构后的setTimeout
示例一样。
结论
当我刚开始接触 React 时,经常会被这个警告困扰。但这个警告彻底改变了我的想法。
如果你好奇,“为什么这种情况只发生在异步函数或任务中”?嗯,这是因为 JavaScript 事件循环。如果你不明白这是什么意思,可以看看Philip Roberts 的 YouTube 视频。
感谢阅读。期待下次再见。请在 Twitter 上点赞并关注我@elijahtrillionz,以便随时保持联系。
鏂囩珷鏉ユ簮锛�https://dev.to/elijahtrillionz/cleaning-up-async-functions-in-reacts-useeffect-hook-unsubscribing-3dkk