React useEffect 内部原理

useEffect 执行时机、依赖数组、清理函数、常见陷阱与 StrictMode 行为

#type / concept #status / growing #tech / dev / frame

React useEffect 内部原理

另见 React useEffectReact useEffect 与 SSR

执行时机

useEffect 在浏览器完成绘制 (paint) 之后异步执行,不阻塞渲染:

render → DOM 更新 → 浏览器绘制 → useEffect 回调执行
useEffect(() => {
  // 这里的代码在浏览器绘制之后执行
  // 适合:数据获取、订阅、手动 DOM 操作
  fetchData();
}, []);

依赖数组对比

// 1. 无依赖数组:每次渲染后都执行
useEffect(() => {
  console.log('每次渲染后执行');
});

// 2. 空数组:仅挂载时执行一次
useEffect(() => {
  console.log('仅挂载时执行');
  return () => console.log('卸载时清理');
}, []);

// 3. 有依赖:依赖变化时执行
useEffect(() => {
  console.log(`count 变为 ${count}`);
  return () => console.log('清理上一次 effect');
}, [count]);

清理函数

useEffect(() => {
  const subscription = someAPI.subscribe(handleData);

  // 清理函数:在下一次 effect 执行前或卸载时调用
  return () => {
    subscription.unsubscribe();
  };
}, []);

清理函数的执行顺序:

  1. 运行新的 effect 之前,先执行上一次 effect 的清理
  2. 组件卸载时,执行当前 effect 的清理

常见陷阱

缺少依赖

// 错误:count 未在依赖数组中,闭包捕获的是初始值
useEffect(() => {
  const timer = setInterval(() => {
    setCount(count + 1); // count 永远是 0
  }, 1000);
  return () => clearInterval(timer);
}, []); // 缺少 count

// 正确:使用函数式更新或添加依赖
useEffect(() => {
  const timer = setInterval(() => {
    setCount(prev => prev + 1); // 不依赖外部 count
  }, 1000);
  return () => clearInterval(timer);
}, []);

陈旧闭包 (Stale Closure)

function ChatRoom({ roomId }) {
  useEffect(() => {
    const conn = createConnection(roomId);
    conn.connect();
    return () => conn.disconnect();
  }, [roomId]); // roomId 变化时重新连接
}

useEffect vs useLayoutEffect

特性useEffectuseLayoutEffect
执行时机浏览器绘制之后DOM 变更后、绘制之前
阻塞渲染
适用场景数据获取、订阅、日志读取 DOM 布局、同步测量
useLayoutEffect(() => {
  // 在浏览器绘制前测量 DOM,避免闪烁
  const { height } = ref.current.getBoundingClientRect();
  setTooltipHeight(height);
}, []);

StrictMode 双重调用

开发模式下,React StrictMode 会故意双重调用 effect 来暴露问题:

挂载 → 执行 effect → 清理 → 再次执行 effect

这意味着 effect 必须是幂等的(执行多次结果相同)。生产环境不会双重调用。

// 好:幂等操作
useEffect(() => {
  const conn = createConnection(roomId);
  conn.connect();
  return () => conn.disconnect(); // 清理函数确保可重复执行
}, [roomId]);

// 差:非幂等(追加 DOM 元素而不清理)
useEffect(() => {
  const div = document.createElement('div');
  document.body.appendChild(div); // 每次调用都会追加!
  // 缺少清理:return () => div.remove();
}, []);
创建于 2026/4/9 更新于 2026/5/27