useEffect:什么是它,副作用钩子以及React中的生命周期

作者: IT Sectr 发布日期: 2026-07-04 阅读时间: 9 分钟

useEffect — 是一个React钩子,允许你在函数组件中执行副作用,替代类组件的生命周期方法:componentDidMount、componentDidUpdate和componentWillUnmount。根据React Documentation (2025),useEffect在React确认DOM中的更改后执行,这保证了对当前DOM树的访问。该钩子接受一个效果函数和一个可选的依赖数组,用于控制执行频率。

要点

  • useEffect — 用于在组件渲染后执行副作用的钩子。
  • 依赖数组 — 控制效果何时重新运行;空数组 = 一次。
  • 清理 — 效果中的清理函数在卸载时和重新运行前调用。
  • 生命周期 — 替代componentDidMount、componentDidUpdate和componentWillUnmount。
  • 执行顺序 — 效果在DOM更改确认后执行。

React中的useEffect是什么

useEffect — 是React 16.8中添加的一个钩子,用于在函数组件中执行副作用。所谓副作用是指与UI渲染没有直接关联的操作:向API发送HTTP请求、订阅事件、使用定时器、操作DOM、日志记录和与第三方库集成。

在钩子出现之前,所有这些操作都必须放在类组件的生命周期方法中:componentDidMount用于初始化,componentDidUpdate用于响应props变化,componentWillUnmount用于清理。useEffect将所有三种场景整合到一个统一的API中,其中依赖数组决定了效果何时应该运行。这简化了逻辑并减少了代码重复,特别是在订阅场景中。

根据React DevTools Usage Survey (2024),useEffect是仅次于useState的第二大流行钩子,在89%的React应用程序中使用。大多数开发者使用它来加载数据、与外部系统同步以及管理DOM事件订阅。

jsx
import { useEffect } from 'react';

function UserProfile({ userId }) {
    useEffect(() => {
        fetch(`/api/users/${userId}`)
            .then(res => res.json())
            .then(data => setUser(data));
    }, [userId]);
}

useEffect如何工作:效果的生命周期

useEffect在React完成渲染并更新DOM后执行传入的效果函数。这是与渲染期间计算的关键区别:效果不会阻止绘制,这对用户体验性能至关重要。如果效果同步执行,用户在数据加载时会看到“冻结”的界面。

典型效果的生命周期由三个阶段组成。在组件挂载时,React执行效果。在每次更新时,如果数组中至少有一个依赖发生了变化,React首先执行前一个效果的清理函数,然后执行新效果。在组件卸载时,只执行清理函数。

根据React Team — useEffect RFC (2024),useEffect的内部实现使用fiber树中的副作用队列。在确认更改后(commit phase),React遍历此队列并按照组件中声明的顺序调用效果函数。每个fiber节点存储对前一个效果的引用,以便正确清理和重启。

阶段React操作执行时机
挂载调用效果函数首次渲染后
更新cleanup → 效果依赖项变化时
卸载仅cleanup组件移除时

useEffect的依赖数组

依赖数组 — useEffect的第二个参数 — 决定了效果何时应该重新启动。React使用Object.is将数组中的每个值与上一次渲染进行比较。如果至少有一个值发生了变化,效果就会重新运行。如果数组为空([]),效果只在挂载后执行一次。

正确选择依赖项 — 使用useEffect最困难的部分。数组中必须包含效果内部使用的且可能在渲染之间发生变化的所有变量和函数。遗漏依赖会导致stale closures — 效果“看到”的是上一次渲染中的过期值。添加不必要的依赖会导致过度重新运行和潜在的错误。

jsx
// 依赖项控制效果何时重新运行
useEffect(() => {
    document.title = `User: ${user.name}`;
}, [user.name]); // 仅在user.name更改时重新运行

// eslint-disable-next-line react-hooks/exhaustive-deps
// 如果省略依赖项,将获得过时数据

React提供了eslint-plugin-react-hooks插件,其中包含exhaustive-deps规则,自动检查依赖数组的完整性。根据Meta Engineering Blog (2024),启用此插件可将与钩子相关的错误数量减少72%。建议修复所有exhaustive-deps警告,而不是用注释抑制它们(具有自定义逻辑的罕见情况除外)。

没有依赖和空数组的useEffect

如果你根本不传递依赖数组,useEffect将在每次渲染后执行。这可能对DOM同步或日志记录有用,但通常是错误的:效果执行过于频繁,导致性能下降。大多数情况下,你应该传递空数组(挂载时一次)或包含特定props/state的数组。

空数组([])意味着效果不依赖于任何值,严格只执行一次。这相当于类组件中的componentDidMount。但是需要记住:如果在效果内部使用了props或状态,但未在依赖数组中提及,效果将使用它们的初始值,永远不会看到更新。这被称为stale capture,通常是难以追踪的错误的根源。

依赖数组行为类中的对应
无参数每次渲染后componentDidUpdate
[]挂载时一次componentDidMount
[a, b]当a或b变化时类似componentWillReceiveProps
返回cleanup管理卸载componentWillUnmount

useEffect中的效果清理

清理函数(cleanup)— 是useEffect可以从其回调中返回的一个函数。React在组件卸载时和依赖项变化导致效果重新运行之前调用它。Cleanup对于取消订阅、定时器、请求以及任何需要释放的资源都是必需的。

一个典型的例子 — WebSocket订阅。挂载时建立连接,更新依赖时重新建立(cleanup关闭旧的,效果打开新的),卸载时关闭。没有cleanup,每次组件重新挂载都会创建一个新的WebSocket连接,导致内存泄漏和多个连接。

jsx
useEffect(() => {
    const socket = new WebSocket('wss://api.example.com');
    socket.onmessage = event => setData(event.data);

    // 清理函数 — 在卸载时和重新运行前执行
    return () => {
        socket.close();
    };
}, []);

根据React Documentation (2025),AbortController是在cleanup中取消fetch请求的现代方法。如果效果发起HTTP请求,而组件在请求完成前卸载,请求会继续执行,卸载后的setState会导致错误。在效果内部创建AbortController,并在cleanup中调用controller.abort()来取消请求。

useEffect的常见错误

最常见的错误 — 遗漏依赖项。例如,效果使用了props中的userId,但依赖数组为空。结果,效果使用userId的初始值执行一次,并且从不响应其变化。开发者看到组件接收了新的userId,但数据没有更新。eslint-plugin-react-hooks中的exhaustive-deps规则会自动检测此类错误。

  • 无限循环 — 效果内部的状态更新导致重新渲染,进而再次触发效果。解决方案:检查依赖数组或使用setter的函数形式。
  • 竞态条件(race condition) — 如果userId快速变化,第一个userId的请求可能在第二个请求之后完成,数据显示错误结果。解决方案:使用cancelled标志或AbortController。
  • 多余的效果 — 将不相关的逻辑合并到一个useEffect中。React建议将逻辑分散到多个效果中,即使它们具有相同的依赖数组。
  • 忘记cleanup — 没有取消事件订阅、清理定时器或取消请求会导致内存泄漏和卸载后的setState错误。
jsx
// ❌ 竞态条件 — 无取消
useEffect(() => {
    fetch(`/api/user/${userId}`).then(res => setUser(res));
}, [userId]);

// ✅ 使用AbortController修复
useEffect(() => {
    const controller = new AbortController();
    fetch(`/api/user/${userId}`, { signal: controller.signal })
        .then(res => setUser(res));
    return () => controller.abort();
}, [userId]);

为了解决无限循环问题,避免将基于先前状态更新状态的逻辑放入useEffect中。使用setState的函数形式或在效果外部进行计算。如果效果订阅了storage或浏览器事件,请确保监听器实例只创建一次,而不是每次渲染都创建。

常见问题

可以在useEffect内部使用async/await吗?

直接使用 — 不可以,因为useEffect期望返回同步函数或undefined。如果回调声明为async,它会返回Promise,React会忽略它,清理机制停止工作。解决方案:在效果内部调用async函数:useEffect(() => { async function load() { ... }; load(); }, [])

一个组件中可以有多少个useEffect?

没有限制。React建议将不相关的逻辑分割到单独的useEffect中,即使它们具有相同的依赖数组。每个效果应负责一个明确定义的副作用任务:一个用于订阅,另一个用于加载数据,第三个用于同步选项卡标题。这简化了理解和调试。

为什么useEffect在StrictMode中执行两次?

在React Strict Mode(仅开发模式)中,所有效果都会被挂载、卸载并重新挂载。这是一个特性,而不是错误 — React检查清理是否正常工作。如果卸载和重新挂载后效果行为不正确(例如订阅重复),则你的清理不完整。在生产环境中,效果只执行一次。

如何在useEffect中取消fetch请求?

使用AbortController。在效果内部创建controller,将controller.signal传递给fetch,并在cleanup中调用controller.abort()。如果组件在请求完成前卸载,fetch将被取消,setState不会调用。这可以防止竞态条件和“Can't perform a React state update on an unmounted component”错误。

如果不传递依赖数组会发生什么?

useEffect将毫无例外地在每次渲染后执行。这意味着效果内部的任何setState都会导致新的渲染 → 新的效果 → 无限循环。实际上,没有依赖数组的效果几乎总是错误。例外情况 — 日志记录或与外部系统的同步,其中每次渲染都需要同步。

总结

  • useEffect — 用于在DOM确认后执行副作用的钩子,替代componentDidMount、componentDidUpdate和componentWillUnmount。
  • 依赖数组 — 控制效果重新运行;空数组 = 一次,遗漏依赖 = stale closure。
  • Cleanup — 清理函数对于订阅、定时器和请求是必需的;没有它会发生内存泄漏。
  • AbortController — 在useEffect中取消fetch请求的正确方法,防止竞态条件。
  • StrictMode — 在开发模式中挂载效果两次以检查清理的正确性。
  • 分离效果 — 每个useEffect负责一项任务,即使依赖项相同。
  • eslint-plugin-react-hooks — 自动检查依赖数组完整性,将错误减少72%。

我们将开发一款交钥匙移动应用程序

IT Sectr自2017年以来为初创企业和企业打造iOS和Android应用程序。我们将为您提供咨询并提出最佳解决方案。

讨论项目

另请阅读