useEffect详解

React 中 useEffect 详解

useEffect 是 React Hooks 中用于处理副作用 的核心 Hook,它替代了类组件中的生命周期方法(componentDidMountcomponentDidUpdatecomponentWillUnmount),让函数组件能够执行异步操作、修改 DOM、订阅事件、清理资源等副作用逻辑。而且现在函数组件才是主流,类组件多是一些老项目

一、核心概念:什么是 "副作用"?

副作用(Side Effect)是指函数执行过程中,除了返回值之外对外部环境产生的影响,常见场景:

  • 数据请求(AJAX/fetch)
  • DOM 操作(修改样式、添加事件监听)
  • 订阅 / 取消订阅(如 WebSocket、定时器)
  • 本地存储(localStorage/sessionStorage)
  • 手动修改 state(需谨慎)

二、基本语法

javascript 复制代码
import { useEffect } from 'react';

function Component() {
  // 基础用法
  useEffect(() => {
    // 副作用逻辑(执行时机:组件挂载/更新后)
    
    // 可选:清理函数(执行时机:组件卸载/下一次副作用执行前)
    return () => {
      // 清理逻辑(如取消订阅、清除定时器)
    };
  }, [依赖项数组]); // 关键:依赖项决定副作用的执行时机
}

语法拆解:

  1. 第一个参数(必选) :副作用函数

    • 可以是普通函数,执行核心的副作用逻辑;
    • 可选返回一个清理函数(Cleanup Function),用于清除副作用(如取消定时器、解绑事件)。
  2. 第二个参数(可选) :依赖项数组

    • 控制副作用的执行时机;
    • 若省略:组件每次渲染后都会执行副作用;
    • 若为空数组 []:仅在组件挂载(mount) 时执行一次,清理函数在组件卸载(unmount) 时执行;
    • 若包含依赖项(如 [count, data]):仅在依赖项的值发生变化时执行副作用(挂载时执行第一次,后续仅依赖项变化触发)。

三、执行时机

useEffect 的执行时机是组件渲染完成后 (浏览器绘制 DOM 之后),属于异步执行,不会阻塞浏览器的渲染,这一点与类组件的 componentDidMount/componentDidUpdate 一致。

不同依赖项的执行行为对比:

依赖项写法 执行时机 对应类组件生命周期
无依赖项 每次组件渲染后(挂载 + 每次更新) componentDidMount + componentDidUpdate
空数组 [] 仅挂载时执行一次 componentDidMount
含依赖项 [a, b] 挂载时执行 + 依赖项 a/b 变化时执行 自定义 componentDidUpdate
清理函数 卸载时执行 + 下一次副作用执行前执行 componentWillUnmount

四、常见使用场景

场景 1:仅挂载时执行(初始化)

比如请求初始数据、添加全局事件监听:

ini 复制代码
import { useEffect, useState } from 'react';

function UserList() {
  const [users, setUsers] = useState([]);

  // 仅挂载时请求数据(对应 componentDidMount)
  useEffect(() => {
    const fetchUsers = async () => {
      const res = await fetch('https://api.example.com/users');
      const data = await res.json();
      setUsers(data);
    };
    fetchUsers();
  }, []); // 空依赖:仅执行一次

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

场景 2:依赖项变化时执行

比如根据输入关键词筛选数据、监听状态变化:

javascript 复制代码
import { useEffect, useState } from 'react';

function Search() {
  const [keyword, setKeyword] = useState('');
  const [result, setResult] = useState([]);

  // 关键词变化时请求数据
  useEffect(() => {
    if (!keyword) return; // 无关键词时不请求

    const timer = setTimeout(async () => {
      const res = await fetch(`https://api.example.com/search?kw=${keyword}`);
      const data = await res.json();
      setResult(data);
    }, 300); // 防抖:300ms 后请求

    // 清理函数:取消定时器(关键词快速变化时避免无效请求)
    return () => clearTimeout(timer);
  }, [keyword]); // 依赖:仅 keyword 变化时执行

  return (
    <div>
      <input value={keyword} onChange={(e) => setKeyword(e.target.value)} />
      <ul>{result.map(item => <li key={item.id}>{item.name}</li>)}</ul>
    </div>
  );
}

场景 3:清理副作用(卸载时)

比如清除定时器、取消 WebSocket 订阅、解绑事件:

javascript 复制代码
import { useEffect, useState } from 'react';

function Timer() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 挂载时启动定时器
    const timer = setInterval(() => {
      setCount(prev => prev + 1);
    }, 1000);

    // 清理函数:卸载时清除定时器(避免内存泄漏)
    return () => clearInterval(timer);
  }, []); // 仅挂载时执行

  return <div>Count: {count}</div>;
}

// 父组件控制 Timer 的挂载/卸载
function Parent() {
  const [show, setShow] = useState(true);
  return (
    <div>
      <button onClick={() => setShow(!show)}>Toggle Timer</button>
      {show && <Timer />}
    </div>
  );
}

五、关键注意事项

1. 依赖项必须完整

React 会根据依赖项数组的浅比较判断是否执行副作用,若依赖项缺失:

  • 副作用可能无法触发(依赖项变化但未声明);
  • 或捕获到过时的 state/prop(闭包陷阱)。

反例(错误)

scss 复制代码
function BadExample() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    // 依赖 count,但未声明在数组中
    const timer = setInterval(() => {
      console.log(count); // 始终打印 0(闭包捕获初始值)
    }, 1000);
    return () => clearInterval(timer);
  }, []); // 错误:缺失 count 依赖

  return <button onClick={() => setCount(count + 1)}>Click</button>;
}

正例(正确)

scss 复制代码
function GoodExample() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // 正确打印最新 count
    }, 1000);
    return () => clearInterval(timer);
  }, [count]); // 声明依赖 count

  return <button onClick={() => setCount(count + 1)}>Click</button>;
}

2. 副作用函数不能是 async 函数

useEffect 的第一个参数若返回清理函数,则不能直接用 async(因为 async 函数返回 Promise,而非清理函数)。

反例(错误)

scss 复制代码
useEffect(async () => {
  const res = await fetch('/api/data');
  // ...
}, []); // 错误:async 函数返回 Promise,无法返回清理函数

正例(正确)

ini 复制代码
useEffect(() => {
  // 内部定义 async 函数
  const fetchData = async () => {
    const res = await fetch('/api/data');
    // ...
  };
  fetchData();
}, []);

3. 避免无限循环

若副作用中修改了依赖项,且依赖项未正确控制,会导致无限执行:

反例(错误)

scss 复制代码
function LoopExample() {
  const [data, setData] = useState({});

  useEffect(() => {
    // 每次渲染都会创建新对象,依赖项浅比较为 false,触发无限执行
    setData({ name: 'test' });
  }, [data]); // 依赖 data,但修改了 data

  return <div>{data.name}</div>;
}

解决方法

  • 避免修改依赖项,或调整依赖项(如仅依赖必要字段);
  • 若必须修改,可使用函数式更新(setData(prev => ({ ...prev, name: 'test' })))。

4. 清理函数的执行时机

清理函数不仅在组件卸载时执行,还会在下一次副作用执行前执行(比如依赖项变化时),用于清除上一次的副作用残留。

六、与类组件生命周期的对比

类组件生命周期 useEffect 实现方式
componentDidMount useEffect(() => { ... }, [])
componentDidUpdate useEffect(() => { ... }, [dep1, dep2])
componentWillUnmount useEffect(() => { return () => { ... } }, [])
合并三者 useEffect(() => { ... })(无依赖项)

总结

useEffect 的核心是声明式地处理副作用:

  1. 通过依赖项数组控制执行时机,替代类组件的多个生命周期;
  2. 清理函数用于消除副作用的副作用(避免内存泄漏);
  3. 依赖项必须完整且准确,避免闭包陷阱和无限循环;
  4. 异步逻辑需在副作用函数内部定义 async 函数,而非直接返回 Promise。
相关推荐
Dr_哈哈2 分钟前
Node.js fs 与 path 完全指南
前端
啊花是条龙7 分钟前
《产品经理说“Tool 分组要一条会渐变的彩虹轴,还要能 zoom!”——我 3 步把它拆成 1024 个像素》
前端·javascript·echarts
C_心欲无痕8 分钟前
css - 使用@media print:打印完美网页
前端·css
青茶36023 分钟前
【js教程】如何用jq的js方法获取url链接上的参数值?
开发语言·前端·javascript
脩衜者38 分钟前
极其灵活且敏捷的WPF组态控件ConPipe 2026
前端·物联网·ui·wpf
Mike_jia43 分钟前
Dockge:轻量开源的 Docker 编排革命,让容器管理回归优雅
前端
GISer_Jing1 小时前
前端GEO优化:AI时代的SEO新战场
前端·人工智能
没想好d1 小时前
通用管理后台组件库-4-消息组件开发
前端
文艺理科生1 小时前
Google A2UI 解读:当 AI 不再只是陪聊,而是开始画界面
前端·vue.js·人工智能
晴栀ay1 小时前
React性能优化三剑客:useMemo、memo与useCallback
前端·javascript·react.js