大家好,我是FogLetter,今天我们来聊聊React中一个非常有趣但又容易被忽视的Hook------useLayoutEffect
。如果你曾经遇到过UI"闪烁"的问题,或者需要在DOM更新后立即获取元素尺寸位置等信息,那么这篇文章就是为你准备的!
从useEffect说起
在开始之前,我们先回顾一下大家更熟悉的useEffect
:
javascript
useEffect(() => {
// 这里的代码会在组件渲染完成后异步执行
console.log('Effect ran');
}, []);
useEffect
是React中最常用的Hook之一,它的执行时机是:
- 组件渲染完成(首次渲染和更新)
- 浏览器完成绘制(页面已经显示给用户)
- 然后React才会执行
useEffect
中的回调函数
这种设计非常合理,因为它不会阻塞浏览器的渲染过程,保证了页面的响应性。但这也带来了一个问题------在某些特定场景下,我们会看到UI的"闪烁"。
useLayoutEffect登场
useLayoutEffect
的API签名和useEffect
完全一样,但执行时机不同:
javascript
useLayoutEffect(() => {
// 这里的代码会在DOM更新后、浏览器绘制前同步执行
console.log('Layout effect ran');
}, []);
关键区别在于:
useLayoutEffect
会在React完成DOM更新后立即同步执行- 它会阻塞浏览器的绘制,直到它的回调函数执行完毕
为什么我们需要useLayoutEffect?
场景1:避免布局闪烁
让我们看第一个例子,这是一个常见的"闪烁"问题:
javascript
function App() {
const [content, setContent] = useState('xx');
const ref = useRef();
useEffect(() => {
setContent('很长的内容很长的内容很长的内容...');
ref.current.style.height = '200px';
}, []);
return (
<div ref={ref} style={{height: '50px', background: 'lightblue'}}>
{content}
</div>
);
}
在这个例子中,你会看到:
- 组件首次渲染,显示短文本和50px高度
- 浏览器绘制这个初始状态
useEffect
执行,更新文本和高度- 浏览器重新绘制
结果是用户会先看到一个短小的蓝色方块,然后突然变成一个大方块------这就是"闪烁"。
解决方案:使用useLayoutEffect
javascript
useLayoutEffect(() => {
setContent('很长的内容很长的内容很长的内容...');
ref.current.style.height = '200px';
}, []);
现在流程变成了:
- 组件首次渲染
- DOM更新后立即执行
useLayoutEffect
,同步更新内容和高度 - 浏览器一次性绘制最终结果
用户再也看不到中间的闪烁状态了!
场景2:精确测量DOM元素
另一个常见场景是需要在渲染后立即获取DOM元素的尺寸或位置:
javascript
function Modal() {
const ref = useRef();
useLayoutEffect(() => {
const height = ref.current.offsetHeight;
const width = ref.current.offsetWidth;
ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`;
ref.current.style.marginLeft = `${(window.innerWidth - width) / 2}px`;
}, []);
return (
<div ref={ref} style={{background:'red', position:'absolute', width:'200px', height:'200px'}}>
我是Modal
</div>
);
}
这里我们需要模态框在屏幕中央,但必须知道它的尺寸才能计算正确的位置。使用useLayoutEffect
可以确保:
- DOM已经更新(模态框已渲染)
- 浏览器尚未绘制(用户看不到未居中的状态)
- 我们同步获取尺寸并设置位置
- 浏览器一次性绘制完美居中的模态框
如果用useEffect
,用户会先看到模态框出现在默认位置,然后突然跳到中央------又一个"闪烁"。
深入理解执行时机
让我们用时间线来更清晰地理解这两个Hook的区别:
markdown
React渲染阶段:
1. 渲染组件(调用组件函数,生成虚拟DOM)
2. 更新真实DOM
useLayoutEffect阶段:
3. 同步执行所有useLayoutEffect回调
4. 浏览器计算布局(但尚未绘制)
useEffect阶段:
5. 浏览器绘制屏幕
6. 异步执行useEffect回调
关键点:
useLayoutEffect
在"绘制前"同步执行useEffect
在"绘制后"异步执行
性能考量
既然useLayoutEffect
可以避免闪烁,为什么React不默认使用它呢?答案在于性能。
useLayoutEffect
的同步特性意味着:
- 它会阻塞浏览器绘制
- 如果回调中有复杂计算,用户会明显感觉到界面卡顿
因此,React团队的建议是:
优先使用useEffect,只有在需要避免视觉不一致时才使用useLayoutEffect
实际开发中的经验
-
动画和过渡 :当实现自定义动画时,
useLayoutEffect
可以确保初始状态和结束状态之间没有不必要的中间帧。 -
工具提示和弹出框定位 :需要根据目标元素位置计算弹出框位置时,
useLayoutEffect
能确保位置计算在用户看到之前完成。 -
自动调整文本区域 :根据内容自动调整textarea高度时,使用
useLayoutEffect
可以避免高度调整时的闪烁。 -
第三方DOM库集成 :当集成像D3.js这样的库时,通常需要在绘制前同步操作DOM,
useLayoutEffect
是理想选择。
常见问题解答
Q:useLayoutEffect会导致性能问题吗? A:只有在回调中有复杂计算时才会。对于简单的DOM操作,性能影响可以忽略不计。
Q:为什么服务端渲染时useLayoutEffect会警告? A:因为服务端没有DOM,useLayoutEffect
无法执行。解决方案是使用useEffect
,或者有条件地使用useLayoutEffect
(仅在客户端)。
Q:可以同时使用useEffect和useLayoutEffect吗? A:可以,它们会按照声明顺序执行(先useLayoutEffect
,后useEffect
)。
总结
useLayoutEffect
是React工具箱中一个强大的工具,虽然不如useEffect
常用,但在特定场景下不可或缺。记住它的特点:
- 同步执行,阻塞渲染
- 适合需要避免UI闪烁的场景
- 适合需要在绘制前读取或修改DOM的场景
- 谨慎使用,避免性能问题
最后,分享一个简单的决策流程图,帮助你决定使用哪个Hook:
markdown
需要操作DOM或避免闪烁吗?
├── 是 → 使用useLayoutEffect
└── 否 → 使用useEffect
希望这篇文章能帮助你更好地理解和使用useLayoutEffect
。如果你有更多有趣的用例或问题,欢迎在评论区分享!
思考题 :你遇到过哪些useEffect
无法解决而必须使用useLayoutEffect
的场景?欢迎分享你的经验!