React useLayoutEffect
1. useLayoutEffect 基本概念
useLayoutEffect 是 React 的一个 Hook,它的函数签名与 useEffect 完全相同,但它会在所有的 DOM 变更之后同步调用 effect。它可以用来读取 DOM 布局并同步触发重渲染。
2. useLayoutEffect vs useEffect
2.1 执行时机对比
Hook 名称 | 执行时机 | 执行方式 | 使用场景 |
---|---|---|---|
useEffect | DOM 更新后且浏览器重新绘制屏幕之后异步执行 (组件渲染完成后) | 异步执行,不阻塞浏览器渲染 | 大多数副作用,如数据获取、订阅 |
useLayoutEffect | DOM 更新后且浏览器重新绘制屏幕之前同步执行(组件将要渲染时) | 同步执行,会阻塞浏览器渲染 | 需要同步测量 DOM 或更新布局 |
2.2 执行顺序示例
function ExampleComponent() {
const [count, setCount] = useState(0);
useEffect(() => {
console.log('useEffect 执行'); // 后执行
});
useLayoutEffect(() => {
console.log('useLayoutEffect 执行'); // 先执行
});
return (
<div onClick={() => setCount(c => c + 1)}>
点击次数:{count}
</div>
);
}
3. useLayoutEffect 使用场景
3.1 DOM 测量和更新
function AutoHeight() {
const [height, setHeight] = useState(0);
const elementRef = useRef();
useLayoutEffect(() => {
// 在 DOM 更新后立即测量高度
const element = elementRef.current;
const elementHeight = element.getBoundingClientRect().height;
if (elementHeight !== height) {
// 立即更新高度,避免闪烁
setHeight(elementHeight);
}
}, [height]);
return (
<div>
<div ref={elementRef} style={{ height: height || 'auto' }}>
内容
</div>
<div>当前高度: {height}px</div>
</div>
);
}
3.2 防止闪烁的工具提示
function Tooltip({ text, position }) {
const tooltipRef = useRef();
const [tooltipPosition, setTooltipPosition] = useState(position);
useLayoutEffect(() => {
const tooltip = tooltipRef.current;
const rect = tooltip.getBoundingClientRect();
// 检查是否超出视口
if (rect.right > window.innerWidth) {
// 立即调整位置,避免闪烁
setTooltipPosition({
...position,
left: position.left - (rect.right - window.innerWidth)
});
}
}, [position]);
return (
<div
ref={tooltipRef}
style={{
position: 'absolute',
...tooltipPosition
}}
>
{text}
</div>
);
}
3.3 动画处理
function AnimatedComponent() {
const elementRef = useRef();
const [isVisible, setIsVisible] = useState(false);
useLayoutEffect(() => {
if (isVisible) {
const element = elementRef.current;
// 立即设置初始状态
element.style.opacity = '0';
element.style.transform = 'translateY(20px)';
// 强制重排
element.getBoundingClientRect();
// 应用动画
element.style.transition = 'opacity 0.5s, transform 0.5s';
element.style.opacity = '1';
element.style.transform = 'translateY(0)';
}
}, [isVisible]);
return (
<div>
<button onClick={() => setIsVisible(true)}>显示</button>
<div ref={elementRef}>
动画内容
</div>
</div>
);
}
3.4 滚动位置同步
function ScrollSync({ content }) {
const scrollContainerRef = useRef();
useLayoutEffect(() => {
const element = scrollContainerRef.current;
// 内容更新后立即滚动到底部
element.scrollTop = element.scrollHeight;
}, [content]); // 当内容更新时执行
return (
<div
ref={scrollContainerRef}
style={{
height: '200px',
overflow: 'auto'
}}
>
{content.map((item, index) => (
<div key={index}>{item}</div>
))}
</div>
);
}
3.5 Modal 定位
function Modal({ isOpen, children }) {
const modalRef = useRef();
useLayoutEffect(() => {
if (isOpen) {
const modalElement = modalRef.current;
const viewportHeight = window.innerHeight;
const modalHeight = modalElement.getBoundingClientRect().height;
// 立即计算并设置最佳位置
modalElement.style.top = \`\${Math.max(
0,
(viewportHeight - modalHeight) / 2
)}px\`;
}
}, [isOpen]);
if (!isOpen) return null;
return (
<div className="modal-overlay">
<div ref={modalRef} className="modal">
{children}
</div>
</div>
);
}
4. 性能考虑
4.1 何时使用 useLayoutEffect
- 需要同步测量 DOM 元素
- 需要在视觉更新前进行 DOM 修改
- 需要避免闪烁或布局抖动
- 处理依赖于 DOM 布局的动画
4.2 何时使用 useEffect
- 数据获取
- 订阅事件
- 日志记录
- 其他不需要同步 DOM 测量或修改的副作用
5. 最佳实践
- 优先使用 useEffect
// ✅ 大多数情况下使用 useEffect 即可
useEffect(() => {
// 异步操作,不影响渲染
fetchData();
}, []);
- 仅在必要时使用 useLayoutEffect
// ✅ 需要同步 DOM 测量和更新时使用 useLayoutEffect
useLayoutEffect(() => {
// 同步操作,立即更新 DOM
updateDOMPosition();
}, []);
- 注意性能影响
// ❌ 避免在 useLayoutEffect 中进行耗时操作
useLayoutEffect(() => {
// 不要在这里进行大量计算或 API 调用
heavyComputation();
}, []);
// ✅ 耗时操作应该放在 useEffect 中
useEffect(() => {
heavyComputation();
}, []);
- 合理使用依赖项
function OptimizedComponent({ data }) {
useLayoutEffect(() => {
// 只在真正需要同步更新的依赖项发生变化时执行
}, [data.layout]); // 只依赖布局相关的属性
}
6. 注意事项
- useLayoutEffect 在服务器端渲染(SSR)中会收到警告,因为它只能在客户端执行
- 过度使用 useLayoutEffect 可能会导致性能问题
- 应该将耗时的操作放在 useEffect 中,只在 useLayoutEffect 中处理视觉相关的同步更新
- 在条件语句中使用时需要注意 Hook 规则
通过合理使用 useLayoutEffect 和 useEffect,我们可以更好地控制副作用的执行时机,优化用户体验,同时保持应用的性能。在实际开发中,应该根据具体场景选择合适的 Hook。