React 中 useEffect 和 useLayoutEffect 选择
React 中 useEffect 与 useLayoutEffect 的选择指南
在 React 函数组件中,useEffect 和 useLayoutEffect 是处理副作用的两个核心 Hook。它们签名完全一致,但执行时机有本质区别。错误的选择可能导致页面闪烁、布局跳动或性能问题。本教程将帮助你精准掌握两者的区别与最佳使用场景。
副作用 Hook 的共同基础
两者都用于在函数组件中执行副作用操作,如数据请求、DOM操作、订阅等。使用方式完全相同:
useEffect(setupFn, dependencies?);
useLayoutEffect(setupFn, dependencies?);
- setupFn: 副作用执行函数,可选择返回一个清理函数。
- dependencies: 依赖数组,控制何时重新执行。
先理解它们适用的共同规则,再分析差异。
useEffect:异步执行,不阻塞渲染
useEffect 在浏览器完成布局与绘制之后异步执行。React 确保先完成屏幕更新,再运行副作用。这意味着:
- 不会阻塞视觉更新,应用保持响应。
- 适合大多数与 DOM 不直接耦合的副作用。
典型执行流程
- React 提交组件树到 DOM。
- 浏览器进行布局计算,绘制像素到屏幕。
- 用户看到最新 UI。
- React 调用
useEffect内的函数。
由于绘制完成后才执行,如果副作用修改了 DOM 并导致新的绘制,用户可能会先看到旧状态,然后很快看到更新后的状态,造成闪烁。因此,对于直接操作 DOM 或需要同步更新布局的场景,useEffect 不是最佳选择。
常见应用场景
- 数据请求:
fetch数据、更新 state,不会引起可见的布局变化。 - 事件订阅: 如
window.addEventListener。 - 手动更改非关键 DOM: 例如修改
document.title、记录日志、分析统计。 - 定时器、计时器:
setInterval,setTimeout。
示例:数据获取
import { useState, useEffect } from 'react';
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
useEffect(() => {
let ignore = false;
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
if (!ignore) setUser(data);
});
return () => { ignore = true; };
}, [userId]);
return <div>{user ? user.name : '加载中...'}</div>;
}
useLayoutEffect:同步执行,在绘制之前
useLayoutEffect 与 useEffect 本质区别在于:它会在 DOM 变更之后、浏览器绘制之前同步调用。换句话说,React 执行完 useLayoutEffect 内的代码后,浏览器才会将内容绘制到屏幕。
执行顺序:
- React 完成 DOM 变更(提交阶段)。
- React 立即同步执行
useLayoutEffect内的函数。 - 浏览器进行布局计算并绘制界面。
用户看到的始终是 useLayoutEffect 执行后的最终状态,因此可以避免视觉上的闪烁。但这也意味着如果内部逻辑繁重,会拖慢浏览器绘制,导致交互卡顿。
典型应用场景
- 需要立即读取布局信息并同步修改 DOM: 例如测量元素尺寸、位置,并基于测量结果更新样式。
- 防止元素跳动或闪烁: 让 UI 在绘制前就处于最终状态。
- 第三方库 DOM 操作: 如使用
scrollIntoView、手动管理过渡动画。 - 当状态变化后必须同步阻止默认行为或执行同步 DOM 调整。
示例:避免模态框闪烁
假设在显示模态框前需要计算其位置,如果在 useEffect 中计算,用户会先看到模态框出现在错误位置,然后跳正。
import { useState, useLayoutEffect, useRef } from 'react';
function Modal() {
const [position, setPosition] = useState({ top: 0, left: 0 });
const modalRef = useRef(null);
const [open, setOpen] = useState(false);
useLayoutEffect(() => {
if (open && modalRef.current) {
const { innerWidth, innerHeight } = window;
const rect = modalRef.current.getBoundingClientRect();
setPosition({
top: (innerHeight - rect.height) / 2,
left: (innerWidth - rect.width) / 2,
});
}
}, [open]);
return open ? (
<div
ref={modalRef}
style={{ position: 'absolute', top: position.top, left: position.left }}
>
模态框内容
</div>
) : null;
}
使用 useLayoutEffect 确保在浏览器绘制前已经调整好位置,用户不会看到未对齐的瞬间。
执行时机对比:记忆口诀
useEffect 在绘制后异步执行;
useLayoutEffect 在绘制前同步执行。
可以通过以下伪时间线理解:
【提交阶段】 -> DOM 更新
↓
【useLayoutEffect 同步】 <-- 此时页面还未绘制
↓
【浏览器 Layout + Paint】
↓
【useEffect 异步】 <-- 画面已显示给用户
选择决策树
根据副作用是否直接影响视觉布局来决定:
- 副作用与视觉布局无关? ➔ 使用
useEffect。 - 需要测量 DOM 并同步应用修改,否则会出现可见的闪烁? ➔ 使用
useLayoutEffect。 - 不确定? 先用
useEffect,仅在出现可见的布局跳动时切换为useLayoutEffect。
注意:useLayoutEffect 内部代码会阻止浏览器绘制,如果包含大量计算或网络请求,会严重降低应用性能。务必保持其内部逻辑轻量、同步。
服务端渲染(SSR)注意事项
useLayoutEffect 在服务端渲染时不会执行,React 会给出警告。如果你的代码需要在服务端运行,要么改用 useEffect,要么使用条件判断保证只在客户端执行:
import { useEffect, useLayoutEffect } from 'react';
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
这样在 SSR 环境下回退到 useEffect,避免错误。
清理函数的行为
两者的清理函数调用时机同样遵循上述规则:
useEffect的清理函数在下一次副作用执行前或组件卸载时调用,且同样是异步的(不阻塞绘制)。useLayoutEffect的清理函数会同步执行,同样发生在重渲染的 DOM 变更之后、新副作用运行之前。
示例:同步清理事件监听器,避免内存泄漏。
useLayoutEffect(() => {
const handleResize = () => {
// 测量并更新布局...
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
综合示例:实时高度测量
要求当内容变化时,容器能立即调整高度且无闪烁:
import { useState, useLayoutEffect, useRef } from 'react';
function AutoHeightPanel({ content }) {
const panelRef = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
if (panelRef.current) {
const newHeight = panelRef.current.scrollHeight;
if (newHeight !== height) {
setHeight(newHeight);
}
}
}, [content, height]);
return (
<div ref={panelRef} style={{ height: height || 'auto', overflow: 'hidden' }}>
{content}
</div>
);
}
如果换成 useEffect,高度变化会在绘制后执行,产生短暂的尺寸跳动。而 useLayoutEffect 让它即刻生效。
性能陷阱与最佳实践
- 避免在 useLayoutEffect 中进行复杂计算: 保持同步代码简洁,如有耗时操作可以将其移到
useEffect或 Web Worker 中。 - 批量读取与写入 DOM: 尽量一次性读取所有需要的布局属性,然后统一写入,减少强制同步布局的触发次数。
- 默认优先选用 useEffect: 绝大多数副作用都不需要影响首次绘制,使用
useEffect能使应用性能更优。 - 当 useState 与 useLayoutEffect 结合时,注意额外的重渲染: 内部调用
setState会触发同步的重新提交,可能导致多次布局计算,谨慎使用。
总结速查表
| 特性 | useEffect |
useLayoutEffect |
|---|---|---|
| 执行阶段 | 绘制后 | 绘制前 |
| 调用方式 | 异步 | 同步 |
| 是否阻塞绘制 | 否 | 是 |
| 视觉闪烁风险 | 可能有 | 避免 |
| 适用场景 | 数据请求、订阅、日志 | 测量、同步 DOM 修改 |
| 性能影响 | 较小 | 可能阻塞渲染 |
| SSR 兼容性 | 正常 | 需特殊处理 |
掌握这一区别后,你就能在 React 组件中精准安排副作用,让界面既流畅又稳定。