React - 【useEffect 与 useLayoutEffect】 区别 及 使用场景

useEffect vs useLayoutEffect 完全指南

🎯 核心区别:执行时机

useEffect(异步副作用)

markdown 复制代码
React 渲染 DOM
    ↓
浏览器绘制屏幕
    ↓
执行 useEffect ← 用户已经看到 DOM
    ↓
可能导致屏幕闪烁

useLayoutEffect(同步副作用)

markdown 复制代码
React 渲染 DOM
    ↓
执行 useLayoutEffect ← 立即同步执行
    ↓
浏览器绘制屏幕(已是最终效果)
    ↓
用户看到最终结果(无闪烁)

📊 详细对比表

特性 useEffect useLayoutEffect
执行时机 DOM 更新后,浏览器绘制前 DOM 更新后,立即同步执行
阻塞渲染 ❌ 不阻塞 ✅ 会阻塞浏览器绘制
性能 ⭐⭐⭐⭐⭐ 更好 ⭐⭐⭐ 可能卡顿
使用频率 99% 的情况用这个 特定场景用
服务端渲染 ✅ 支持 ❌ 会报警告
清理函数执行 DOM 更新前执行 DOM 更新前执行

🔴 useEffect 执行时间线

javascript 复制代码
function Component() {
  useEffect(() => {
    console.log('1. useEffect 执行');
    return () => console.log('2. useEffect 清理函数');
  }, []);

  console.log('0. 组件渲染');
  return <div>Hello</div>;
}

// 执行顺序:
// 0. 组件渲染
// 1. useEffect 执行
// 2. useEffect 清理函数(卸载或依赖变化时)

🟠 useLayoutEffect 执行时间线

javascript 复制代码
function Component() {
  useLayoutEffect(() => {
    console.log('1. useLayoutEffect 执行(同步)');
    return () => console.log('2. useLayoutEffect 清理函数');
  }, []);

  console.log('0. 组件渲染');
  return <div>Hello</div>;
}

// 执行顺序:
// 0. 组件渲染
// 1. useLayoutEffect 执行(同步,阻塞浏览器绘制)
// 浏览器绘制屏幕
// 2. useLayoutEffect 清理函数

💡 常用场景 & 代码示例

场景1️⃣:数据请求(useEffect)✅ 推荐

javascript 复制代码
function UserProfile({ userId }) {
  const [user, setUser] = useState(null);
  const [loading, setLoading] = useState(false);

  useEffect(() => {
    setLoading(true);
    
    // 异步获取数据
    fetch(`/api/users/${userId}`)
      .then(res => res.json())
      .then(data => {
        setUser(data);
        setLoading(false);
      })
      .catch(err => {
        console.error('加载失败:', err);
        setLoading(false);
      });
  }, [userId]);  // userId 变化时重新获取

  if (loading) return <div>加载中...</div>;
  return <div>{user?.name}</div>;
}

为什么用 useEffect?

  • 数据获取是异步操作,不需要同步阻塞
  • 用户看到加载状态很正常

场景2️⃣:事件监听(useEffect)✅ 推荐

javascript 复制代码
function WindowResize() {
  const [width, setWidth] = useState(window.innerWidth);

  useEffect(() => {
    function handleResize() {
      setWidth(window.innerWidth);
    }

    // 添加监听
    window.addEventListener('resize', handleResize);

    // 清理:移除监听
    return () => {
      window.removeEventListener('resize', handleResize);
    };
  }, []);  // 只在挂载时运行

  return <div>窗口宽度: {width}px</div>;
}

为什么用 useEffect?

  • 事件监听是异步的,不需要同步执行
  • 必须清理事件监听器,防止内存泄漏

场景3️⃣:DOM 测量(useLayoutEffect)✅ 正确

javascript 复制代码
function MeasureElement() {
  const ref = useRef(null);
  const [height, setHeight] = useState(0);

  useLayoutEffect(() => {
    // 必须用 useLayoutEffect,否则会闪烁
    // 1. 测量 DOM 尺寸
    const rect = ref.current.getBoundingClientRect();
    
    // 2. 基于尺寸计算布局
    const newHeight = rect.width > 500 ? 300 : 150;
    
    // 3. 立即更新(在浏览器绘制前)
    setHeight(newHeight);
  }, []);  // 只在挂载时运行

  return (
    <div ref={ref} style={{ height: `${height}px` }}>
      响应式高度
    </div>
  );
}

为什么用 useLayoutEffect?

  • 需要在浏览器绘制前完成 DOM 测量和布局计算
  • 如果用 useEffect,用户会看到高度从 0 闪到 300(闪烁!)

场景4️⃣:表单焦点设置(useLayoutEffect)✅ 正确

javascript 复制代码
function AutoFocusForm() {
  const inputRef = useRef(null);

  useLayoutEffect(() => {
    // 必须用 useLayoutEffect,否则表单打开时看不到光标在哪
    inputRef.current?.focus();
  }, []);

  return (
    <form>
      <input ref={inputRef} placeholder="自动聚焦" />
      <button type="submit">提交</button>
    </form>
  );
}

为什么用 useLayoutEffect?

  • 如果用 useEffect,用户会看到输入框出现,然后光标才聚焦(视觉延迟)
  • useLayoutEffect 保证光标在浏览器绘制前已经聚焦

场景5️⃣:动画初始化(useLayoutEffect)✅ 正确

javascript 复制代码
function AnimatedBox() {
  const boxRef = useRef(null);

  useLayoutEffect(() => {
    // 设置初始状态(隐藏)
    gsap.set(boxRef.current, { opacity: 0, x: -50 });
    
    // 然后执行动画
    gsap.to(boxRef.current, { 
      opacity: 1, 
      x: 0, 
      duration: 0.5 
    });
  }, []);

  return <div ref={boxRef}>动画盒子</div>;
}

为什么用 useLayoutEffect?

  • 避免用户看到初始未变换的状态,然后才动画
  • useLayoutEffect 保证在浏览器绘制前完成初始设置

场景6️⃣:主题切换(useLayoutEffect)⚠️ 特殊

javascript 复制代码
function ThemeProvider({ theme }) {
  useLayoutEffect(() => {
    // 立即应用主题,避免闪烁
    document.documentElement.style.colorScheme = theme;
    document.body.className = `theme-${theme}`;
  }, [theme]);

  return <div>内容</div>;
}

为什么用 useLayoutEffect?

  • 防止主题切换时的闪烁(深色→浅色 的白屏闪烁)

📋 依赖数组详解

javascript 复制代码
// 1️⃣ 每次渲染都执行(没有依赖数组)
useEffect(() => {
  console.log('每次都运行');
});

// 2️⃣ 只在挂载时执行(空依赖数组)
useEffect(() => {
  console.log('只在挂载时执行');
}, []);

// 3️⃣ 依赖值变化时执行
useEffect(() => {
  console.log('userId 变化时执行');
}, [userId]);

// 4️⃣ 多个依赖
useEffect(() => {
  console.log('userId 或 pageNum 变化时执行');
}, [userId, pageNum]);

// 5️⃣ 依赖是对象时的坑
useEffect(() => {
  // ❌ 每次都会执行,因为 {} 每次都是新的
  console.log('可能无限循环');
}, [{}]);

// ✅ 正确做法:使用 useMemo
const config = useMemo(() => ({}), []);
useEffect(() => {
  console.log('config 稳定,不会无限循环');
}, [config]);

⚠️ 常见陷阱

陷阱1️⃣:useLayoutEffect 导致卡顿

javascript 复制代码
// ❌ 不好:useLayoutEffect 中做复杂计算会阻塞绘制
useLayoutEffect(() => {
  // 这会冻结浏览器!
  for (let i = 0; i < 1000000; i++) {
    complexCalculation();
  }
}, []);

// ✅ 好:复杂计算放到 useEffect
useEffect(() => {
  for (let i = 0; i < 1000000; i++) {
    complexCalculation();
  }
}, []);

陷阱2️⃣:useLayoutEffect 在服务端渲染中报错

javascript 复制代码
// ❌ 错:SSR 时会报警告
useLayoutEffect(() => {
  // SSR 中不能执行
}, []);

// ✅ 好:检测环境
useEffect(() => {
  if (typeof window !== 'undefined') {
    // 客户端代码
  }
}, []);

// 或者条件性使用
const useIsomorphicLayoutEffect = 
  typeof window !== 'undefined' ? useLayoutEffect : useEffect;

useIsomorphicLayoutEffect(() => {
  // 自动适配 SSR
}, []);

陷阱3️⃣:忘记清理副作用

javascript 复制代码
// ❌ 坏:内存泄漏
useEffect(() => {
  const timer = setInterval(() => {
    console.log('每秒执行');
  }, 1000);
  // 没有清理!
}, []);

// ✅ 好:返回清理函数
useEffect(() => {
  const timer = setInterval(() => {
    console.log('每秒执行');
  }, 1000);
  
  return () => clearInterval(timer);  // 清理定时器
}, []);

陷阱4️⃣:无限循环

javascript 复制代码
// ❌ 坏:无限循环
useEffect(() => {
  setCount(count + 1);  // 每次都修改 count
}, [count]);  // 导致重新渲染,又触发 effect

// ✅ 好:不把更新的值放在依赖中
useEffect(() => {
  setCount(c => c + 1);
}, []);  // 空依赖,只执行一次

🚀 选择流程图

markdown 复制代码
需要执行副作用?
    ↓
是异步操作(数据获取、定时器)?
    ├─ 是 → useEffect ✅
    └─ 否 ↓
    
需要在浏览器绘制前同步执行?
    ├─ 是 → useLayoutEffect ✅
    │   例:DOM 测量、焦点、动画、主题
    └─ 否 → useEffect ✅(默认选择)

📊 性能对比

javascript 复制代码
// 测试性能影响

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

  // useEffect 不会阻塞渲染
  useEffect(() => {
    // 即使这里做复杂计算,页面也能快速响应
    for (let i = 0; i < 10000000; i++) {}
  }, [count]);

  // useLayoutEffect 会阻塞渲染
  useLayoutEffect(() => {
    // 这个计算会让点击按钮时出现明显延迟
    for (let i = 0; i < 10000000; i++) {}
  }, [count]);

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

// 结论:
// useEffect 按钮响应迅速
// useLayoutEffect 点击时会卡顿

✅ 快速参考

99% 情况用 useEffect

javascript 复制代码
// 数据获取
useEffect(() => { fetchData(); }, []);

// 事件监听
useEffect(() => { 
  window.addEventListener('resize', handler);
  return () => window.removeEventListener('resize', handler);
}, []);

// 副作用通用场景
useEffect(() => { doSomething(); }, [dep]);

特殊场景用 useLayoutEffect

javascript 复制代码
// DOM 测量
useLayoutEffect(() => { 
  const rect = ref.current.getBoundingClientRect();
  setHeight(rect.height);
}, []);

// 焦点设置
useLayoutEffect(() => { 
  inputRef.current?.focus();
}, []);

// 动画初始化
useLayoutEffect(() => { 
  gsap.set(ref.current, initialState);
  startAnimation();
}, []);

🎓 总结

维度 useEffect useLayoutEffect
何时用 99% 的情况 需要同步 DOM 操作
执行时机 浏览器绘制后 浏览器绘制前
是否阻塞 不阻塞 会阻塞
性能 优秀 可能卡顿
例子 数据请求、事件监听 DOM 测量、焦点、动画

记住:优先用 useEffect,只有特殊场景才用 useLayoutEffect! 🎯

相关推荐
5***o5001 小时前
React安全
前端·安全·react.js
喵个咪2 小时前
Qt 6 实战:C++ 调用 QML 回调方法(异步场景完整实现)
前端·c++·qt
F***c3252 小时前
React自然语言处理应用
前端·react.js·自然语言处理
1***Q7842 小时前
React项目
前端·javascript·react.js
幸福专买店2 小时前
【Flutter】flutter 中 包裹内容显示 的设置方式
前端·javascript·flutter
U***49833 小时前
React Native性能分析
javascript·react native·react.js
和和和3 小时前
🗣️面试官: 那些常见的前端面试场景问题
前端·javascript·面试
lxp1997413 小时前
vue笔记摘要-更新中
前端·vue.js·笔记
Oriental3 小时前
URL解码踩坑记录
前端·后端