你是不是遇到过这种情况:页面刚渲染出来时,某些元素的位置或尺寸会突然"跳一下",出现短暂闪烁?或者明明代码逻辑没问题,但UI更新总感觉慢半拍?
这很可能是因为你没选对React的副作用钩子!今天我们就来彻底搞懂useLayoutEffect和useEffect的区别,让你从此告别页面闪烁问题。
浏览器绘制时机的秘密
要理解这两个钩子的区别,得先知道浏览器是怎么工作的。当React更新DOM时,浏览器其实做了两件事:
首先是布局计算 :浏览器计算每个元素的位置和大小 然后是实际绘制:把计算好的像素点画到屏幕上
useEffect是在浏览器完成绘制之后 才执行的,相当于"事后诸葛亮"。而useLayoutEffect则是在DOM更新后、浏览器绘制之前执行的,相当于"现场指挥"。
这么说可能有点抽象,我们来看个具体例子。
代码实战:测量DOM尺寸并立即更新
假设我们需要实时获取一个div的宽度,并根据宽度调整样式。先用useEffect试试:
javascript
function MyComponent() {
const [width, setWidth] = useState(0);
const divRef = useRef(null);
useEffect(() => {
// 在useEffect中测量元素尺寸
if (divRef.current) {
const measuredWidth = divRef.current.offsetWidth;
setWidth(measuredWidth);
console.log('useEffect中测量的宽度:', measuredWidth);
}
}, []);
return (
<div
ref={divRef}
style={{
width: '100%',
height: 100,
backgroundColor: width > 500 ? 'blue' : 'red'
}}
>
当前宽度: {width}px
</div>
);
}
运行这段代码,你会发现页面上的div颜色可能会从红色瞬间变成蓝色(或者反过来),出现一次闪烁。这是因为:
- 组件首次渲染时,width是0,div显示红色
- 浏览器绘制了红色div
- useEffect执行,测量到实际宽度并更新state
- 组件重新渲染,div颜色根据新宽度改变
- 浏览器再次绘制,颜色变化
用户就能看到这次颜色变化,造成闪烁。
现在改用useLayoutEffect:
javascript
function MyComponent() {
const [width, setWidth] = useState(0);
const divRef = useRef(null);
useLayoutEffect(() => {
// 在useLayoutEffect中测量元素尺寸
if (divRef.current) {
const measuredWidth = divRef.current.offsetWidth;
setWidth(measuredWidth);
console.log('useLayoutEffect中测量的宽度:', measuredWidth);
}
}, []);
return (
<div
ref={divRef}
style={{
width: '100%',
height: 100,
backgroundColor: width > 500 ? 'blue' : 'red'
}}
>
当前宽度: {width}px
</div>
);
}
这次就不会有闪烁了!因为:
- 组件首次渲染时,width是0,div显示红色
- 在浏览器绘制之前,useLayoutEffect就执行了
- 它测量宽度并立即更新state
- 组件基于正确的宽度重新渲染
- 浏览器一次性绘制最终效果
用户直接看到最终状态,没有中间闪烁过程。
千万不要在useLayoutEffect里做这些事!
useLayoutEffect虽然能解决闪烁问题,但有个重要陷阱:它会阻塞浏览器渲染!
因为useLayoutEffect执行期间,浏览器一直在等待,直到它执行完才能继续绘制。如果你在里面执行耗时操作,用户就会感觉页面卡住了。
来看个反面教材:
javascript
function SlowComponent() {
const [data, setData] = useState(null);
useLayoutEffect(() => {
// 错误示范:在useLayoutEffect中执行耗时操作
const fetchData = async () => {
// 模拟一个耗时的网络请求
await new Promise(resolve => setTimeout(resolve, 2000));
setData('加载完成的数据');
};
fetchData();
}, []);
return <div>{data || '加载中...'}</div>;
}
这段代码会让用户面对一个空白页面整整2秒钟!因为useLayoutEffect阻塞了渲染,直到数据获取完成后,浏览器才能绘制页面。
正确的做法是:
javascript
function CorrectComponent() {
const [data, setData] = useState(null);
useEffect(() => {
// 正确:在useEffect中执行耗时操作
const fetchData = async () => {
await new Promise(resolve => setTimeout(resolve, 2000));
setData('加载完成的数据');
};
fetchData();
}, []);
return <div>{data || '加载中...'}</div>;
}
这样至少用户能立即看到"加载中..."的提示,知道页面正在工作。
什么时候用哪个?一张图帮你决定
简单来说,记住这个原则:
用useLayoutEffect:当你需要直接操作DOM或避免视觉闪烁时 比如:测量元素尺寸、调整元素位置、同步更新状态以避免闪烁
用useEffect:当你需要执行副作用且不关心绘制时机时 比如:数据获取、订阅事件、手动更改文档标题
再分享一个实际开发中的经验:如果你不确定该用哪个,先试试useEffect。只有在发现视觉闪烁问题时,才考虑改用useLayoutEffect。
真实案例:自定义Tooltip组件
假设我们要实现一个自定义的Tooltip组件,需要根据目标元素的位置来计算Tooltip的显示位置。
用useEffect实现:
javascript
function Tooltip({ children, content }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const targetRef = useRef(null);
const tooltipRef = useRef(null);
useEffect(() => {
if (targetRef.current && tooltipRef.current) {
const targetRect = targetRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
// 计算Tooltip位置,避免超出屏幕
let top = targetRect.bottom + 5;
let left = targetRect.left;
// 如果Tooltip会超出右边界,调整位置
if (left + tooltipRect.width > window.innerWidth) {
left = window.innerWidth - tooltipRect.width - 5;
}
setPosition({ top, left });
}
}, [content]);
return (
<>
<span ref={targetRef}>{children}</span>
<div
ref={tooltipRef}
style={{
position: 'fixed',
top: position.top,
left: position.left,
display: content ? 'block' : 'none'
}}
>
{content}
</div>
</>
);
}
这个实现会有闪烁问题:Tooltip会先出现在默认位置(0,0),然后突然跳到正确位置。
改用useLayoutEffect:
javascript
// 只需要把上面的useEffect换成useLayoutEffect
useLayoutEffect(() => {
// ...同样的位置计算逻辑
}, [content]);
现在Tooltip就会直接出现在正确位置,没有任何闪烁!
总结一下
useEffect和useLayoutEffect就像两个性格不同的助手:
useEffect是"不紧不慢型",等浏览器画完页面再干活,适合不紧急的任务 useLayoutEffect是"雷厉风行型",必须在浏览器画画前把事情搞定,适合紧急的DOM操作
记住关键区别:useLayoutEffect会阻塞浏览器渲染,useEffect不会
现在你应该明白怎么选择了吧?下次遇到页面闪烁问题,就知道该请哪位"助手"出马了!
你在项目中还遇到过哪些关于useEffect和useLayoutEffect的坑?欢迎在评论区分享你的经历和解决方案~