useEffect 完整使用指南:依赖数组、闭包陷阱、清理函数实战

useEffect 完整使用指南:依赖数组、闭包陷阱、清理函数实战

useEffect 是 React 中最核心、也最容易让人困惑的 Hook 之一。它用于处理副作用:数据获取、订阅、手动修改 DOM、计时器等。

本文将系统性地讲解 useEffect 的工作原理,并重点拆解依赖数组、闭包陷阱、清理函数这三大难点,配合实战示例,帮助你真正掌握它。


一、useEffect 是什么?

在函数组件中,没有 componentDidMount、componentDidUpdate 这些生命周期了。useEffect 就是用来替代它们的。

副作用(Side Effect) :一切"影响组件外部"的操作,比如:

  • 请求接口
  • 操作 DOM
  • 设置定时器
  • 订阅事件

基本语法

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

二、useEffect 的执行时机

理解 useEffect,首先要理解它的执行时机。

javascript 复制代码
function App() {
  console.log('render');

  useEffect(() => {
    console.log('effect');
  });

  return <div>App</div>;
}

执行顺序:

  1. render
  2. DOM 更新完成
  3. effect

✅ useEffect 是异步执行的,不会阻塞浏览器渲染。


三、依赖数组:控制 Effect 的触发

useEffect 的第二个参数------依赖数组 ,决定了 effect 何时重新执行。

1️⃣ 不传依赖数组(每次渲染都执行)

javascript 复制代码
useEffect(() => {
  console.log('每次渲染都执行');
});

⚠️ 等价于 componentDidUpdate + componentDidMount,非常容易导致性能问题。


2️⃣ 空依赖数组(只执行一次)

scss 复制代码
useEffect(() => {
  console.log('组件挂载时执行一次');
}, []);

✅ 等价于 componentDidMount

常用于:

  • 页面初始化请求
  • 初始化第三方库

3️⃣ 有依赖的数组(依赖变化时执行)

scss 复制代码
useEffect(() => {
  console.log('count changed:', count);
}, [count]);

✅ 等价于 componentDidUpdate(但只监听 count)


四、依赖数组的常见误区

❌ 误区一:依赖写少了

scss 复制代码
useEffect(() => {
  setCount(count + 1);
}, []); // ❌ 忘记写 count

React 会警告你,因为 count 被使用了,却没加入依赖。

✅ 正确写法:

scss 复制代码
useEffect(() => {
  setCount(count + 1);
}, [count]);

✅ 原则:effect 中用到的所有响应式值,都必须放进依赖数组


❌ 误区二:依赖写多了

scss 复制代码
useEffect(() => {
  fetchData();
}, [fetchData]);

如果 fetchData 每次渲染都重新创建,effect 就会无限执行。

✅ 解决方案:使用 useCallback

scss 复制代码
const fetchData = useCallback(() => {
  // ...
}, []);

useEffect(() => {
  fetchData();
}, [fetchData]);

五、闭包陷阱(最常见、最隐蔽)

这是 useEffect 最让人头疼的问题。

问题示例

javascript 复制代码
function Counter() {
  let count = 0;

  useEffect(() => {
    const timer = setInterval(() => {
      console.log(count); // 永远是 0
    }, 1000);
  }, []);

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

为什么?

  • useEffect 在第一次渲染时执行
  • 它捕获了当时的 count = 0
  • 后续点击更新的是新的闭包
  • 定时器里的 count 永远不会变

✅ 解决方案一:使用 useState

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

useEffect(() => {
  const timer = setInterval(() => {
    console.log(count);
  }, 1000);
  return () => clearInterval(timer);
}, [count]);

✅ 每次 count 变化,effect 重新执行,拿到最新值。


✅ 解决方案二:使用 useRef(获取最新值)

ini 复制代码
const countRef = useRef(0);

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

const increment = () => {
  countRef.current++;
};

✅ 适合:不希望触发重新渲染,但需要访问最新值。


六、清理函数(Cleanup Function)

清理函数用于防止内存泄漏,在以下时机执行:

  • 组件卸载时
  • effect 重新执行前

基本示例:清除定时器

javascript 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('running');
  }, 1000);

  return () => {
    clearInterval(timer);
  };
}, []);

示例:取消事件监听

javascript 复制代码
useEffect(() => {
  const handler = () => console.log('resize');
  window.addEventListener('resize', handler);

  return () => {
    window.removeEventListener('resize', handler);
  };
}, []);

⚠️ 注意:清理函数不是"卸载时才执行"

javascript 复制代码
useEffect(() => {
  console.log('effect');
  return () => console.log('cleanup');
}, [count]);

执行顺序:

  1. cleanup(上一次)
  2. render
  3. effect(这一次)

七、useEffect 的实战场景

1️⃣ 数据请求(最常用)

ini 复制代码
useEffect(() => {
  let ignore = false;

  fetch('/api/user')
    .then(res => res.json())
    .then(data => {
      if (!ignore) {
        setUser(data);
      }
    });

  return () => {
    ignore = true; // 防止竞态问题
  };
}, [userId]);

✅ 防止组件卸载后 setState 报错。


2️⃣ 同步 URL 参数

ini 复制代码
useEffect(() => {
  const params = new URLSearchParams(window.location.search);
  const q = params.get('q');
  setKeyword(q);
}, []);

3️⃣ 页面标题修改

ini 复制代码
useEffect(() => {
  document.title = `当前计数:${count}`;
}, [count]);

八、useEffect vs useLayoutEffect

对比项 useEffect useLayoutEffect
执行时机 渲染后(异步) DOM 更新后、浏览器绘制前
会阻塞渲染 ❌ ✅
使用场景 大多数副作用 测量 DOM、同步布局

✅ 99% 的场景用 useEffect 就够了


九、useEffect 使用原则(总结)

✅ 原则一:effect 中用到的所有变量,必须加入依赖数组

✅ 原则二 :能用 [] 就不用 [deps]

✅ 原则三:清理函数一定要写,防止内存泄漏

✅ 原则四:避免不必要的 effect(能用派生状态就不用 effect)

✅ 原则五:不要把 effect 当"生命周期"用,而要当成"同步副作用"


十、一句话总结

useEffect 的本质不是"执行副作用",而是把组件内部的 state / props 与外部系统同步。

当你从"同步"的角度去理解 useEffect,闭包陷阱、依赖数组、清理函数都会变得清晰自然。

相关推荐
ServBay15 小时前
基于Jev的浏览器Agent插件狂揽 21k star,3分钟教你解放双手
后端·aigc·ai编程
Json____15 小时前
家居装修 AI 智能咨询助手:让装修咨询从“大海捞针“变成“一问即答“
java·后端·vue3·it学习·wwwoop.com
Apifox16 小时前
Apifox 9 月更新|CLI 能力升级、GitLab 私有化部署接入与产品体验优化
前端·后端·测试
鱼弦16 小时前
Agent智能体 vs 传统运维:职业天花板的3倍差距?
后端
暗夜行者之光16 小时前
LangGraph 实战:用 LangSmith 可视化追踪 AI 智能体执行轨迹
后端
鱼弦16 小时前
Agent 的工具选择策略:从硬编码到动态决策
后端
Lost of 程序猿16 小时前
建造者模式实战:告别“十参数构造函数“的数据导出任务
后端·设计模式·c#·asp.net
SensorFlow16 小时前
ClickHouse 埋点事件表:先验证时间与重复,再谈漏斗看板
后端
Dreams_l17 小时前
基于SpringBoot实现的抽奖系统(一)
java·spring boot·后端
余槐i18 小时前
rollbackFor 不设置,Checked 异常不回滚:Spring 默认规则与两行修复的取舍
java·spring boot·后端·spring·事务管理