避免 React useEffect 中的竞争条件和内存泄漏

2025-05-26

避免 React useEffect 中的竞争条件和内存泄漏

让我们学习如何处理“无法对未安装的组件执行 React 状态更新”警告

让我们看一下从 API 请求获取数据的实现,看看此组件中是否存在发生竞争条件的可能性:

import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition() {
  const [todo, setTodo] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
      const newData = await response.json();
      setTodo(newData);
    };
    fetchData();
  }, []);
  if (data) {
    return <div>{data.title}</div>;
  } else {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

我们指定了一个空数组作为useEffect React hook的依赖项。这样就确保了获取请求只发生一次。但是这个组件仍然容易出现竞争条件和内存泄漏。这是怎么回事?

如果 API 服务器响应时间过长,并且在收到响应之前组件已被卸载,则会发生内存泄漏。尽管组件已被卸载,但请求完成后仍会收到响应。响应随后会被解析并调用 setTodo 方法。React 会抛出以下警告:

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

其信息非常直接。

同一问题的另一个潜在场景可能是待办事项列表 ID 被作为 prop 传入。

import React, { useEffect} from 'react';
export default function UseEffectWithRaceCondition( {id} ) {
  const [todo, setTodo] = useState(null);
  useEffect(() => {
    const fetchData = async () => {
      const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`);
      const newData = await response.json();
      setTodo(newData);
    };
    fetchData();
  }, [id]);
  if (data) {
    return <div>{data.title}</div>;
  } else {
    return null;
  }
}
Enter fullscreen mode Exit fullscreen mode

如果钩子在请求完成之前收到了不同的 ID,并且第二个请求在第一个请求之前完成,我们将在组件中显示第一个请求的数据。

竞争条件问题的潜在解决方案

有几种方法可以解决这个问题。这两种方法都利用了 useEffect 提供的清理功能。

  • 我们可以使用布尔值标志来确保组件已挂载。这样,只有当标志为 true 时,我们才会更新状态。如果我们在组件内部发出多个请求,我们将始终显示最后一个请求的数据。

  • 我们可以使用 AbortController 在组件卸载时取消之前的请求。不过 IE 不支持 AbortController。所以如果要使用这种方法,我们需要考虑这一点。

使用布尔标志进行 useEffect 清理

useEffect(() => {
  let isComponentMounted = true;
    const fetchData = async () => {
      const response = await fetch('https://jsonplaceholder.typicode.com/todos/1');
      const newData = await response.json();
      if(isComponentMounted) {
        setTodo(newData);
      }
    };
    fetchData();
    return () => {
      isComponentMounted = false;
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

此修复依赖于 useEffect 的清理函数的工作方式。如果组件渲染多次,则会在执行下一个 effect 之前清理上一个 effect。

由于这种工作方式,它对于我们另一个由于 ID 被更改而产生的多个请求的示例也能正常工作。我们仍然会遇到竞争条件,因为后台会有多个请求在执行。但只有最后一个请求的结果才会显示在 UI 上。

使用 AbortController 进行 useEffect 清理

虽然之前的方法有效,但它并不是处理竞争条件的最佳方法。请求在后台进行。后台保留过时的请求会不必要地消耗用户的带宽。而且浏览器还会限制最大并发请求数(最多 6-8 个)。

从我们之前关于如何取消 HTTP 获取请求的文章中,我们了解到 DOM 标准中新增的 AbortController API。我们可以利用它来中止我们的请求。

useEffect(() => {
  let abortController = new AbortController();
    const fetchData = async () => {
      try {
        const response = await fetch('https://jsonplaceholder.typicode.com/todos/1', {
            signal: abortController.signal,
          });
      const newData = await response.json();
        setTodo(newData);
      }
      catch(error) {
         if (error.name === 'AbortError') {
          // Handling error thrown by aborting request
        }
      }
    };
    fetchData();
    return () => {
      abortController.abort();
    }
  }, []);
Enter fullscreen mode Exit fullscreen mode

由于中止请求会引发错误,我们需要明确处理它。

这个解决方案和上一个类似。在重新渲染的情况下,清理函数会在执行下一个 effect 之前执行。不同之处在于,由于我们使用了 AbortController,浏览器也会取消请求。

以上就是我们在使用 React 的 useEffect hook 发起 API 请求时避免竞争条件的两种方法。如果你想使用一些允许取消请求的第三方库,可以使用 Axios 或 ReactQuery,它们也提供了许多其他功能。

如果您有任何疑问,请在下面发表评论。

最初于 2021 年 2 月 8 日发布于https://www.wisdomgeek.com 。

文章来源:https://dev.to/saranshk/avoiding-race-conditions-and-memory-leaks-in-react-useeffect-3mme
PREV
为什么选择 GraphQL?
NEXT
Var、let 和 const——有什么区别?