从菜鸟到大神:搞定 useEffect 和 useLayoutEffect,让页面不再‘抖’起来!

136 阅读7分钟

在 React 开发中,useEffectuseLayoutEffect 是两个非常关键的 Hook,它们都用于处理副作用(side effects),但其执行时机和适用场景截然不同。本文将从底层机制到实际应用,系统性地讲解这两个 Hook 的区别、使用逻辑以及如何在项目中做出合理选择。

通过这篇文章,你将获得:

  • 对 useEffect 与 useLayoutEffect 的深入理解
  • React 渲染流程中的生命周期阶段划分
  • 两者的底层执行顺序与浏览器渲染机制的关系
  • 常见应用场景与优化建议
  • 多个实用示例,涵盖布局调整、防止闪烁、性能优化等
  • 面试问题及详细解答

一、基础概念:什么是副作用?

在函数组件中,副作用(Side Effect) 是指那些不会直接影响组件返回值,但会影响外部状态的操作,例如:

  • 数据获取(API 请求)
  • 手动修改 DOM
  • 添加/移除事件监听器
  • 设置定时器或动画
  • 订阅数据源(如 WebSocket)

为了支持这些操作,React 提供了两个专门的 Hook 来管理副作用:useEffectuseLayoutEffect

deepseek_mermaid_20250711_d32bee.png

二、useEffect 与 useLayoutEffect 的核心区别

特性useEffectuseLayoutEffect
触发时机在浏览器完成绘制后异步执行在 DOM 更新完成后同步执行,但在浏览器重绘前
是否阻塞页面渲染❌ 不阻塞✅ 阻塞
适合场景大多数副作用操作,如 API 请求、非即时 DOM 操作布局测量、样式计算、避免视觉闪烁
执行优先级较低较高

三、React 渲染流程简析:它们到底在什么时候执行?

要彻底理解两者的区别,必须了解 React 的渲染流程。

📈 React 组件的完整生命周期(简化版)

  1. 开始渲染阶段(Render Phase)
    • React 根据当前状态生成虚拟 DOM。
    • 调用组件函数并收集副作用钩子(effect hooks)。
  2. 提交阶段(Commit Phase)
    • React 将虚拟 DOM 变化反映到真实 DOM 上。
    • 执行所有 useLayoutEffect 中定义的回调。
    • 浏览器进行重新绘制(painting)。
    • 执行所有 useEffect 中定义的回调(延迟到下一轮宏任务中执行)。

🔍 总结一句话:

useLayoutEffect 在 DOM 更新后立即执行(同步),而 useEffect 在浏览器完成绘制后再执行(异步)。


四、底层机制对比:为什么一个阻塞,一个不阻塞?

🕒 useLayoutEffect 是同步执行的

  • 它运行在 DOM 更新之后,浏览器尚未绘制之前。
  • 此时你可以安全地读取 DOM 的最新尺寸、位置等信息。
  • 如果你在其中执行耗时操作(比如频繁修改 DOM 或复杂计算),会直接阻塞浏览器绘制,导致页面卡顿。

useEffect 是异步执行的

  • 它会在浏览器完成绘制后,通过 requestIdleCallback 或宏任务队列来调度执行。
  • 这意味着它不会影响用户看到的内容,也不会造成视觉上的“抖动”或“闪烁”。

💡 通俗类比:

  • useLayoutEffect 就像在上菜前检查菜品摆放是否正确,如果不对就马上调整。
  • useEffect 则是菜已经端上桌了,再回头看一眼有没有需要改进的地方。

五、典型应用场景对比

✅ 使用 useEffect 的场景

  • 发起网络请求(fetch)
  • 添加全局事件监听器(如 resize、scroll)
  • 设置和清理定时器(setTimeout / setInterval)
  • 日志上报、埋点追踪
  • 非即时更新的 DOM 操作(如添加 class、动态修改属性)
useEffect(() => {
  const timer = setTimeout(() => {
    console.log('3秒后执行');
  }, 3000);
  
  return () => clearTimeout(timer);
}, []);

✅ 使用 useLayoutEffect 的场景

  • 获取 DOM 元素的实际尺寸(offsetWidth / offsetHeight)
  • 动态设置元素位置(如弹窗居中、tooltip 定位)
  • 防止因 DOM 未完全更新导致的视觉闪烁
  • 需要在浏览器重绘前完成的同步计算
const ref = useRef();

useLayoutEffect(() => {
  const height = ref.current.offsetHeight;
  ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`;
}, []);

return <div ref={ref} style={{ width: '200px', background: 'lightblue' }}>居中弹窗</div>;

六、实战案例分析

🎯 场景一:动态调整弹窗位置,防止闪烁

如果我们使用 useEffect 来调整弹窗的位置,可能会出现“先偏移,再跳回”的闪烁问题。因为此时浏览器已经完成了绘制,用户已经看到了初始位置。

function Modal() {
  const ref = useRef();

  useEffect(() => {
    const height = ref.current.offsetHeight;
    ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`;
  }, []);

  return (
    <div ref={ref} style={{
      position: 'absolute',
      left: '50%',
      transform: 'translateX(-50%)',
      background: 'red',
      color: 'white'
    }}>
      弹窗内容
    </div>
  );
}

改用 useLayoutEffect:DOM 更新完毕后立刻调整位置,浏览器在绘制时就已经是最终位置,避免闪烁。


🎯 场景二:测量 DOM 元素尺寸

如果你需要根据某个容器的宽度来决定内部子元素的排布方式,就必须使用 useLayoutEffect 来确保拿到的是最新的尺寸。

function Container() {
  const containerRef = useRef();

  useLayoutEffect(() => {
    const width = containerRef.current.offsetWidth;
    if (width > 600) {
      // 设置大屏布局
    } else {
      // 设置小屏适配
    }
  }, []);

  return <div ref={containerRef}>内容区域</div>;
}

七、性能优化建议

⚠️ 谨慎使用 useLayoutEffect

由于它是同步执行的,任何耗时操作都会阻塞页面渲染。以下是一些优化技巧:

  • 尽量减少 useLayoutEffect 中的计算量。
  • 对于不需要精确布局的操作,优先使用 useEffect
  • 如果确实需要多次测量,考虑使用 ResizeObserver 替代。

✅ 使用依赖项控制执行频率

无论是 useEffect 还是 useLayoutEffect,都应该传入正确的依赖数组(deps array),以避免不必要的重复执行。

useLayoutEffect(() => {
  // ...
}, [dependency]); // 只有 dependency 变化时才执行

八、扩展知识:React 的 Layout 与 Paint 阶段

🧩 React 的渲染流程分为两个主要阶段:

  1. Render Phase(渲染阶段)

    • 构建虚拟 DOM 树
    • 执行组件函数
    • 收集副作用钩子
  2. Commit Phase(提交阶段)

    • 更新真实 DOM
    • 执行 useLayoutEffect
    • 浏览器进行绘画(Painting)
    • 执行 useEffect

📌 这个过程也被称为 “React 的 Commit Lifecycle”,是理解副作用执行顺序的关键。


九、何时该选哪个 Hook?

需求推荐 Hook理由
获取 DOM 尺寸、位置useLayoutEffect必须在浏览器绘制前获取准确信息
修改样式以防止闪烁useLayoutEffect避免视觉不一致
发送 API 请求useEffect不影响视觉表现,延迟执行更合理
添加事件监听器useEffect可异步注册不影响首屏渲染
设置定时器useEffect不影响首次渲染速度
动态调整布局useLayoutEffect避免布局抖动

deepseek_mermaid_20250711_a4adaf.png


十、总结

对比维度useEffectuseLayoutEffect
是否同步执行
是否阻塞页面渲染
触发时机浏览器绘制后DOM 更新后,浏览器绘制前
性能影响大(需谨慎使用)
推荐用途数据请求、事件监听、日志埋点布局测量、样式调整、防止闪烁

十一、相关问题

Q1: useEffectuseLayoutEffect 的主要区别是什么?

A: 主要区别在于它们的执行时机:

  • useEffect 在浏览器完成绘制后异步执行,适用于大多数副作用处理场景。
  • useLayoutEffect 在 DOM 更新完成后同步执行,在浏览器重绘前,适用于需要基于最新 DOM 状态进行操作的场景。

Q2: 在什么情况下应该使用 useLayoutEffect 而不是 useEffect

A: 当你需要基于最新的 DOM 状态进行操作,例如测量 DOM 节点尺寸、调整布局以防止视觉闪烁时,应使用 useLayoutEffect。因为它保证了在浏览器重绘前执行,避免了视觉上的不一致。

Q3: 如何避免 useLayoutEffect 导致的性能问题?

A: 减少 useLayoutEffect 中的计算量,仅在必要时使用它。对于不需要精确布局的操作,优先使用 useEffect。此外,可以利用 ResizeObserver 等替代方案来优化性能。

Q4: useEffectuseLayoutEffect 的依赖项是如何工作的?

A: 依赖项数组决定了 Hook 的回调函数何时重新执行。如果数组为空,则只在组件挂载和卸载时调用;如果有值,则每当依赖项变化时都会重新执行 Hook。


希望这篇文章不仅帮你理清了 useEffectuseLayoutEffect 的工作原理,还能帮助你在实际开发中做出更合理的决策,并为即将到来的技术面试做好准备。如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、分享给更多开发者朋友。我们下次再见!