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

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

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

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


一、useEffect 是什么?

在函数组件中,没有 componentDidMountcomponentDidUpdate 这些生命周期了。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,闭包陷阱、依赖数组、清理函数都会变得清晰自然。

相关推荐
智驭未来掌门人3 小时前
在windows下快速搭建go2rtc流媒体平台
后端
大白804 小时前
为什么不要滥用 useMemo 和 useCallback?过度优化反而更卡
后端
神奇小汤圆4 小时前
Codex 子 Agent 配置指南:让 Sol 当军师,Luna 当搬砖工
后端
ClouGence4 小时前
2026 年 4 款数据库管理工具推荐:免费、开源、付费怎么选?
数据库·后端·开源
大勇前进5 小时前
React 中 State 为什么不能直接修改?很多新手一直没懂
后端
大黄评测5 小时前
useState 与 useReducer 该怎么选?别只会无脑用 useState
后端
乐橙开放平台5 小时前
物业 SaaS 笔记:子账号 Policy 按通道隔离,At_ 管控面 / St_ 数据面治理 accessToken
后端·物联网·音视频
站大爷IP5 小时前
Python的is和==把我坑惨了,原来对象比较的水这么深
后端
fatcoder5 小时前
玩转Nginx 04 — 反向代理:给 nginx 接上后端
前端·后端·nginx