如果你会用useEffect
(精读React hook(五):useEffect使用细节知多少?),那么你一定也会用useLayoutEffect
。因为它们的用法一模一样。
useEffect
和useLayoutEffect
的区别仅有一个:
useEffect
执行时机是在React的渲染和提交阶段之后;useLayoutEffect
执行时机是在React的提交阶段之后,但在浏览器实际绘制屏幕之前。
useEffect vs useLayoutEffect
我们通过一个例子来看看阻塞和非阻塞对用户来说有什么区别。
jsx
import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';
function BoxComparison() {
const [heightEffect, setHeightEffect] = useState(0);
const [heightLayoutEffect, setHeightLayoutEffect] = useState(0);
const refEffect = useRef(null);
const refLayoutEffect = useRef(null);
useEffect(() => {
if (refEffect.current) {
setHeightEffect(refEffect.current.offsetWidth);
}
}, []);
useLayoutEffect(() => {
if (refLayoutEffect.current) {
setHeightLayoutEffect(refLayoutEffect.current.offsetWidth);
}
}, []);
return (
<div>
<div>
<h2>使用 useEffect</h2>
<div ref={refEffect} style={{ width: '200px', height: '50px', background: 'lightgray' }}>这是一个方块</div>
<div style={{ width: '100px', height: `${heightEffect}px`, background: 'red', marginTop: '10px' }}>红色方块</div>
</div>
<div style={{ marginTop: '30px' }}>
<h2>使用 useLayoutEffect</h2>
<div ref={refLayoutEffect} style={{ width: '200px', height: '50px', background: 'lightgray' }}>这是一个方块</div>
<div style={{ width: '100px', height: `${heightLayoutEffect}px`, background: 'blue', marginTop: '10px' }}>蓝色方块</div>
</div>
</div>
);
}
export default BoxComparison;
这个例子写了两个方块,分别使用useEffect
和useLayoutEffect
来更新高度,代码实际效果在我的演示站查看。
当你在性能较差的设备上肉眼可以明显看到区别:
useEffect
的方块会闪一下useLayoutEffect
的方块则不会闪
如果你的电脑性能比较好,可以尝试多次刷新,也有一定几率看到useEffect
的闪动。
我们应该这样描述二者的区别:
useEffect
: 执行时机是在React的渲染和提交阶段之后。这意味着当任何相关DOM更改被应用并且组件已被重新渲染后,useEffect
里的代码会执行。但它是异步的,所以可能会在浏览器的下一个绘制周期之后才执行。useLayoutEffect
: 执行时机是在React的提交阶段之后,但在浏览器实际绘制屏幕之前。这使得你可以同步地读取或更改DOM,然后让浏览器在下一次绘制时立即体现这些更改,从而避免不必要的闪烁或布局跳动。
useLayoutEffect的作用
我们已经清楚了useLayoutEffect
的特性了,那么可以猜想,useLayoutEffect
是作用于这样的场景:需要在浏览器绘制前获取 DOM 元素的大小或位置,或者在浏览器绘制前修改 DOM。
这里有一个非常典型的场景------tooltip 组件。我们就来写一个 tooltip 组件,应用useLayoutEffect
来自适应设置 tooltip 位置。
我们的需求是:鼠标移入一个按钮,能够判断 tooltip 展示区域,如果按钮上方空间足够,则显示在上方,如果按钮上方空间不够,则自适应显示在按钮下方。
为了保证没有页面抖动,我们要使用useLayoutEffect
来更新显示的位置,示例代码如下:
jsx
import React, { useLayoutEffect, useRef, useState } from "react";
import { createPortal } from "react-dom";
export default function HoverTooltip() {
const containerRef = useRef(null);
return (
<div
ref={containerRef}
className="p-8 bg-gray-100 w-full rounded-xl mt-5 shadow-lg m-4 space-y-4 overflow-hidden"
>
<ButtonWithTooltip
containerRef={containerRef}
tooltipContent="This tooltip does not fit above the button. This is why it's displayed below instead!"
>
Hover over me (tooltip above)
</ButtonWithTooltip>
<ButtonWithTooltip
containerRef={containerRef}
tooltipContent="This tooltip fits above the button"
>
Hover over me (tooltip below)
</ButtonWithTooltip>
<ButtonWithTooltip
containerRef={containerRef}
tooltipContent="This tooltip fits above the button"
>
Hover over me (tooltip below)
</ButtonWithTooltip>
</div>
);
}
const ButtonWithTooltip = ({ tooltipContent, containerRef, children }) => {
const [targetRect, setTargetRect] = useState(null);
const [containerRect, setContainerRect] = useState(null);
const buttonRef = useRef(null);
return (
<div className="relative">
<button
ref={buttonRef}
className="py-2 px-4 bg-blue-500 text-white rounded hover:bg-blue-600 active:bg-blue-700 focus:outline-none transition"
onMouseEnter={() => {
buttonRef.current &&
setTargetRect(buttonRef.current.getBoundingClientRect());
containerRef.current &&
setContainerRect(containerRef.current.getBoundingClientRect());
}}
onMouseLeave={() => setTargetRect(null)}
>
{children}
</button>
{targetRect && containerRect && (
<Tooltip targetRect={targetRect} containerRect={containerRect}>
{tooltipContent}
</Tooltip>
)}
</div>
);
};
const Tooltip = ({ children, targetRect, containerRect }) => {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
if (ref.current) {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // 设置高度
}
}, [children]);
let tooltipX = targetRect.left;
let tooltipY =
targetRect.top - containerRect.top - tooltipHeight < 0
? targetRect.bottom
: targetRect.top - tooltipHeight; // 计算位置
return createPortal(
<div
ref={ref}
className="absolute bg-gray-700 text-white py-1 px-2 rounded shadow-md"
style={{
left: `${tooltipX}px`,
top: `${tooltipY}px`,
}}
>
{children}
</div>,
document.body
);
};
这里示例中,我们写了三个按钮,每次鼠标移入按钮的时候,计算按钮到父级上沿的空间是否可以容纳一个 tooltip,如果足够,tooltip 就在按钮上方展示,如果不够,则在按钮下方展示。实际效果如图:
你也可以到演示站试一试。
总结
最后,我们再明确一下useEffect
和useLayoutEffect
分别在何时使用、useLayoutEffect
的使用注意事项。
何时使用useEffect
- 副作用与DOM无关:例如,数据获取、设置订阅、手动更改浏览器的URL等。
- 不需要立即同步读取或更改DOM :如果你不关心可能的微小布局跳动或闪烁,那么
useEffect
就足够了。 - 性能考虑 :
useEffect
通常对性能影响较小,因为它不会阻塞浏览器渲染。
何时使用useLayoutEffect
- 需要同步读取或更改DOM:例如,你需要读取元素的大小或位置并在渲染前进行调整。
- 防止闪烁 :在某些情况下,异步的
useEffect
可能会导致可见的布局跳动或闪烁。例如,动画的启动或某些可见的快速DOM更改。 - 模拟生命周期方法 :如果你正在将旧的类组件迁移到功能组件,并需要模拟
componentDidMount
、componentDidUpdate
和componentWillUnmount
的同步行为。
使用注意事项
- 避免过度使用
useLayoutEffect
,因为它是同步的,可能会影响应用的性能。只有当你确实需要同步的DOM操作时才使用它。 - 如果代码在服务器端渲染(SSR)中出现问题,考虑回退到
useEffect
。useLayoutEffect
在服务器端渲染时不会运行,可能会引发警告或错误。
系列文章列表
精读React hook(一):useState 的几个基础用法和进阶技巧
精读React hook(二):React状态管理的强大工具------useReducer
精读React hook(三):useContext从基础应用到性能优化
精读React hook(五):useEffect使用细节知多少?
精读React hook(六):useLayoutEffect解决了什么问题?
未完待续......