【大前端】React useEffect 详解:从入门到进阶

React useEffect 详解:从入门到进阶

在 React Hooks 出现之前,我们经常通过 生命周期函数componentDidMountcomponentDidUpdatecomponentWillUnmount 等)来管理副作用逻辑。

React 16.8 起,Hooks 引入了 useEffect,统一了副作用处理逻辑,使函数组件具备了管理副作用的能力。


1. 什么是副作用(Side Effect)?

在 React 中,副作用 指的是那些会影响函数组件之外环境的操作,比如:

  • 数据请求(Ajax / fetch / axios)
  • DOM 操作(手动修改元素属性)
  • 订阅/取消订阅(WebSocket、事件监听器)
  • 定时器(setIntervalsetTimeout

这些逻辑如果直接写在函数体里,会导致 多次执行、状态不一致 等问题,因此 React 提供了 useEffect 来专门管理副作用。


2. useEffect 基本语法

jsx 复制代码
useEffect(() => {
  // 副作用逻辑
  return () => {
    // 清理逻辑(可选)
  };
}, [依赖项]);

参数解析

  • 第一个参数:一个函数,包含副作用逻辑。

    • 可返回一个清理函数,用于组件卸载或依赖变化时执行清理。
  • 第二个参数:依赖数组(dependency array)。

    • [] 空数组 → 仅在初次渲染执行一次(类似 componentDidMount)。
    • [state, props] → 当依赖项变化时执行(类似 componentDidUpdate)。
    • 省略 → 每次渲染后都执行。

3. 使用场景举例

3.1 模拟 componentDidMount

jsx 复制代码
useEffect(() => {
  console.log("组件挂载完成");
}, []);

3.2 模拟 componentDidUpdate

jsx 复制代码
useEffect(() => {
  console.log("count 变化了:", count);
}, [count]);

3.3 模拟 componentWillUnmount

jsx 复制代码
useEffect(() => {
  const id = setInterval(() => console.log("定时器"), 1000);
  return () => clearInterval(id); // 清理逻辑
}, []);

4. 常见坑点

4.1 依赖数组遗漏

jsx 复制代码
useEffect(() => {
  fetch(`/api/user/${id}`);
}, []); 

❌ 错误:id 变化时不会重新请求

✅ 正确:

jsx 复制代码
useEffect(() => {
  fetch(`/api/user/${id}`);
}, [id]);

4.2 无限循环陷阱

jsx 复制代码
useEffect(() => {
  setCount(count + 1); // 修改状态
}, [count]);

⚠️ 会导致无限循环更新。

👉 解决:需要加条件判断,或用 useRef 缓存不影响渲染的变量。


4.3 异步函数处理

useEffect 不能直接传入 async 函数:

jsx 复制代码
// ❌ 错误写法
useEffect(async () => {
  const res = await fetchData();
}, []);

✅ 正确写法:

jsx 复制代码
useEffect(() => {
  async function loadData() {
    const res = await fetchData();
    setData(res);
  }
  loadData();
}, []);

5. useEffect vs useLayoutEffect

  • useEffect:异步执行,不会阻塞浏览器渲染(大多数场景使用它)。
  • useLayoutEffect:同步执行,DOM 更新后立即运行,常用于需要精确操作 DOM 的场景。

6. 最佳实践

  1. 依赖项必须写全:避免因闭包导致取到旧值。
  2. 拆分 effect :不同逻辑不要写在一个 useEffect,保证单一职责。
  3. 避免频繁触发 :对频繁变化的值,可以结合 useDebounceuseThrottle
  4. 合理使用清理函数:确保事件监听、定时器等不会造成内存泄漏。

7. 总结

  • useEffect 是 React 用来管理副作用的统一接口。
  • 依赖数组的使用是关键,决定了副作用的执行时机。
  • 清理函数是避免内存泄漏的利器。
  • 面试常问陷阱:依赖数组遗漏无限循环更新异步写法错误

📌 推荐一个思考题(常见面试题):

为什么下面的 useEffect 打印的总是旧的 count

jsx 复制代码
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(id);
}, []);

这是一个典型的 闭包陷阱 (stale closure) 问题。我们来拆解一下原因和解决办法:


🔍 原因

你的代码:

jsx 复制代码
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(id);
}, []);

关键点:

  • useEffect 的依赖数组是 [],所以只会在组件挂载时执行一次。
  • setInterval 回调函数创建时,它"捕获"了当时的 count 变量。
  • 之后即使 count 在组件中更新了,回调函数里的 count 依旧是老的值,因为闭包绑定的是初始快照。

所以 console.log(count) 打印的总是旧的。


✅ 解决办法

方法 1:把 count 放到依赖数组里

jsx 复制代码
useEffect(() => {
  const id = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(id);
}, [count]);

这样,每次 count 更新时,useEffect 会重新执行,重新注册一个新的定时器,拿到最新的 count

⚠️ 但这种方式会不断清除和重建定时器,有时不太优雅。


方法 2:使用函数式更新 + useRef

利用 useRef 保存最新的 count

jsx 复制代码
const countRef = useRef(count);

useEffect(() => {
  countRef.current = count;
}, [count]);

useEffect(() => {
  const id = setInterval(() => {
    console.log(countRef.current);
  }, 1000);
  return () => clearInterval(id);
}, []);

这里定时器只建立一次,但回调里取的是 countRef.current,它会随着 count 更新而变化。


方法 3:函数式 setState

如果逻辑允许,可以用函数式更新避免依赖旧状态:

jsx 复制代码
setCount(prev => prev + 1);

这样不依赖闭包里的旧值。


📌 总结

  • 原因 :闭包导致定时器回调捕获的是旧的 count

  • 解决方案

    1. count 放到依赖数组 → 每次更新重建 effect。
    2. useRef 保存最新值 → 定时器只建一次但能读到最新数据。
    3. 用函数式 setState 避免依赖旧值。

相关推荐
小只笨笨狗~16 分钟前
el-dialog宽度根据内容撑开
前端·vue.js·elementui
weixin_4903543420 分钟前
Vue设计与实现
前端·javascript·vue.js
GISer_Jing1 小时前
React过渡更新:优化渲染性能的秘密
javascript·react.js·ecmascript
烛阴1 小时前
带你用TS彻底搞懂ECS架构模式
前端·javascript·typescript
卓码软件测评2 小时前
【第三方网站运行环境测试:服务器配置(如Nginx/Apache)的WEB安全测试重点】
运维·服务器·前端·网络协议·nginx·web安全·apache
龙在天2 小时前
前端不求人系列 之 一条命令自动部署项目
前端
开开心心就好2 小时前
PDF转长图工具,一键多页转图片
java·服务器·前端·数据库·人工智能·pdf·推荐算法
国家不保护废物2 小时前
10万条数据插入页面:从性能优化到虚拟列表的终极方案
前端·面试·性能优化
文心快码BaiduComate3 小时前
七夕,画个动态星空送给Ta
前端·后端·程序员
web前端1233 小时前
# 多行文本溢出实现方法
前端·javascript