在 React 开发中,useEffect 和 useLayoutEffect 是两个非常关键的 Hook,它们都用于处理副作用(side effects),但其执行时机和适用场景截然不同。本文将从底层机制到实际应用,系统性地讲解这两个 Hook 的区别、使用逻辑以及如何在项目中做出合理选择。
通过这篇文章,你将获得:
- ✅ 对 useEffect 与 useLayoutEffect 的深入理解
- ✅ React 渲染流程中的生命周期阶段划分
- ✅ 两者的底层执行顺序与浏览器渲染机制的关系
- ✅ 常见应用场景与优化建议
- ✅ 多个实用示例,涵盖布局调整、防止闪烁、性能优化等
- ✅ 面试问题及详细解答
一、基础概念:什么是副作用?
在函数组件中,副作用(Side Effect) 是指那些不会直接影响组件返回值,但会影响外部状态的操作,例如:
- 数据获取(API 请求)
- 手动修改 DOM
- 添加/移除事件监听器
- 设置定时器或动画
- 订阅数据源(如 WebSocket)
为了支持这些操作,React 提供了两个专门的 Hook 来管理副作用:useEffect 和 useLayoutEffect。
二、useEffect 与 useLayoutEffect 的核心区别
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 触发时机 | 在浏览器完成绘制后异步执行 | 在 DOM 更新完成后同步执行,但在浏览器重绘前 |
| 是否阻塞页面渲染 | ❌ 不阻塞 | ✅ 阻塞 |
| 适合场景 | 大多数副作用操作,如 API 请求、非即时 DOM 操作 | 布局测量、样式计算、避免视觉闪烁 |
| 执行优先级 | 较低 | 较高 |
三、React 渲染流程简析:它们到底在什么时候执行?
要彻底理解两者的区别,必须了解 React 的渲染流程。
📈 React 组件的完整生命周期(简化版)
- 开始渲染阶段(Render Phase)
- React 根据当前状态生成虚拟 DOM。
- 调用组件函数并收集副作用钩子(effect hooks)。
- 提交阶段(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 的渲染流程分为两个主要阶段:
-
Render Phase(渲染阶段)
- 构建虚拟 DOM 树
- 执行组件函数
- 收集副作用钩子
-
Commit Phase(提交阶段)
- 更新真实 DOM
- 执行
useLayoutEffect - 浏览器进行绘画(Painting)
- 执行
useEffect
📌 这个过程也被称为 “React 的 Commit Lifecycle”,是理解副作用执行顺序的关键。
九、何时该选哪个 Hook?
| 需求 | 推荐 Hook | 理由 |
|---|---|---|
| 获取 DOM 尺寸、位置 | useLayoutEffect | 必须在浏览器绘制前获取准确信息 |
| 修改样式以防止闪烁 | useLayoutEffect | 避免视觉不一致 |
| 发送 API 请求 | useEffect | 不影响视觉表现,延迟执行更合理 |
| 添加事件监听器 | useEffect | 可异步注册不影响首屏渲染 |
| 设置定时器 | useEffect | 不影响首次渲染速度 |
| 动态调整布局 | useLayoutEffect | 避免布局抖动 |
十、总结
| 对比维度 | useEffect | useLayoutEffect |
|---|---|---|
| 是否同步执行 | ❌ | ✅ |
| 是否阻塞页面渲染 | ❌ | ✅ |
| 触发时机 | 浏览器绘制后 | DOM 更新后,浏览器绘制前 |
| 性能影响 | 小 | 大(需谨慎使用) |
| 推荐用途 | 数据请求、事件监听、日志埋点 | 布局测量、样式调整、防止闪烁 |
十一、相关问题
Q1: useEffect 和 useLayoutEffect 的主要区别是什么?
A: 主要区别在于它们的执行时机:
useEffect在浏览器完成绘制后异步执行,适用于大多数副作用处理场景。useLayoutEffect在 DOM 更新完成后同步执行,在浏览器重绘前,适用于需要基于最新 DOM 状态进行操作的场景。
Q2: 在什么情况下应该使用 useLayoutEffect 而不是 useEffect?
A: 当你需要基于最新的 DOM 状态进行操作,例如测量 DOM 节点尺寸、调整布局以防止视觉闪烁时,应使用 useLayoutEffect。因为它保证了在浏览器重绘前执行,避免了视觉上的不一致。
Q3: 如何避免 useLayoutEffect 导致的性能问题?
A: 减少 useLayoutEffect 中的计算量,仅在必要时使用它。对于不需要精确布局的操作,优先使用 useEffect。此外,可以利用 ResizeObserver 等替代方案来优化性能。
Q4: useEffect 和 useLayoutEffect 的依赖项是如何工作的?
A: 依赖项数组决定了 Hook 的回调函数何时重新执行。如果数组为空,则只在组件挂载和卸载时调用;如果有值,则每当依赖项变化时都会重新执行 Hook。
希望这篇文章不仅帮你理清了 useEffect 和 useLayoutEffect 的工作原理,还能帮助你在实际开发中做出更合理的决策,并为即将到来的技术面试做好准备。如果你觉得这篇文章对你有帮助,欢迎点赞、收藏、分享给更多开发者朋友。我们下次再见!