清理 React useEffect Hook 中的异步函数(取消订阅)

2025-06-08

清理 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
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

useEffect根据函数的第二个参数来调用函数的回调函数useEffect

第二个参数是依赖项数组。你可以在这里列出你的依赖项。

因此,只要任何依赖项有更新,就会调用回调函数。

useEffect(() => {
  if (loading) {
    setUsername('Stranger');
  }
}, [loading]);
Enter fullscreen mode Exit fullscreen mode

如果依赖项数组为空(就像我们的第一个示例一样),则 React 将只调用该函数一次,即在组件安装时。

但是你可能会想,“当它卸载时,React 不也会调用该函数吗?”。

嗯,不。返回的函数是一个闭包,当你在需要的函数(现在的返回函数)中访问父函数的作用域时,你实际上不需要调用父函数(现在是回调函数)。

如果您对此还不清楚,请花 7 分钟时间阅读我撰写的有关JavaScript 闭包的文章。

现在我们已经回顾了基础知识,让我们来看看异步函数的问题。

React 中的异步函数

毫无疑问,你可能曾经在 useEffect hook 中使用过异步函数。如果你还没有用过,那么你很快就会用到它。

但是,当我们在 useEffect hook 中使用异步函数时,卸载并挂载组件时,React 会发出一个警告。这是警告

无法对已卸载的组件执行 React 状态更新。这是一个无操作,但它表明您的应用程序存在内存泄漏。要修复此问题,请在 useEffect 清理函数中取消所有订阅和异步任务。

如果你看不到图片,这里是警告

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.
Enter fullscreen mode Exit fullscreen mode

指令非常清晰明了:“在 useEffect 的清理函数中取消所有订阅和异步任务”。好吧,我明白你的 React 心思了!但是我该怎么做呢?

很简单,非常简单。React 抛出这个警告的原因是因为我在 async 函数中使用了 setState。

这不算什么罪过。但 React 会在组件卸载后尝试更新状态,这算是一种罪过(泄漏罪)。

这是导致上述警告的代码

useEffect(() => {
  setTimeout(() => {
    setUsername('hello world');
  }, 4000);
}, []);
Enter fullscreen mode Exit fullscreen mode

我们该如何解决这个问题?我们只需告诉 React 仅在挂载时尝试更新异步函数中的任何状态。

因此我们有

useEffect(() => {
  let mounted = true;
  setTimeout(() => {
    if (mounted) {
      setUsername('hello world');
    }
  }, 4000);
}, []);
Enter fullscreen mode Exit fullscreen mode

好的,现在我们取得了一些进展。目前我们只是告诉 React,如果mounted(你可以称之为已订阅或其他)为 true,则执行更新。

但是该mounted变量始终为 true,因此无法防止警告或应用程序泄漏。那么,我们该如何以及何时将其设置为 false?

当组件卸载时,我们可以也应该将其设置为 false。所以我们现在有

useEffect(() => {
  let mounted = true;
  setTimeout(() => {
    if (mounted) {
      setUsername('hello world');
    }
  }, 4000);

  return () => mounted = false;
}, []);
Enter fullscreen mode Exit fullscreen mode

因此,当组件卸载时,mounted变量将更改为 false,因此setUsername在组件卸载时该函数将不会更新。

我们可以通过看到的第一段代码来判断组件何时挂载和卸载。

useEffect(() => {
  // actions performed when component mounts

  return () => {
    // actions to be performed when component unmounts
  }
}, []);
Enter fullscreen mode Exit fullscreen mode

这是取消订阅异步函数的方法,您可以通过不同的方式执行此操作,例如

useEffect(() => {
  let t = setTimeout(() => {
    setUsername('hello world');
  }, 4000);

  return () => clearTimeout(t);
}, []);
Enter fullscreen mode Exit fullscreen mode

这是一个带有 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
}, []);
Enter fullscreen mode Exit fullscreen mode

更新:正如@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();
}, []);
Enter fullscreen mode Exit fullscreen mode

现在 React 将不会尝试更新该setUsername函数,因为请求已被中止。就像重构后的setTimeout示例一样。

结论

当我刚开始接触 React 时,经常会被这个警告困扰。但这个警告彻底改变了我的想法。

如果你好奇,“为什么这种情况只发生在异步函数或任务中”?嗯,这是因为 JavaScript 事件循环。如果你不明白这是什么意思,可以看看Philip Roberts 的 YouTube 视频

感谢阅读。期待下次再见。请在 Twitter 上点赞并关注我@elijahtrillionz,以便随时保持联系。

鏂囩珷鏉ユ簮锛�https://dev.to/elijahtrillionz/cleaning-up-async-functions-in-reacts-useeffect-hook-unsubscribing-3dkk
PREV
2021 年 10 个重要的全栈 Web 开发工具
NEXT
使用 JavaScript 对象构建项目。