在 React 的魔法世界里,有两个极其相似的咒语:
useEffect
和useLayoutEffect
。它们就像魔法学院里的一对双胞胎兄弟,外表相似但性格迥异。今天,我们就来揭开这对"特效双雄"的神秘面纱!
先看这对兄弟的简历
useEffect(异步侠)
- 出场时机:组件渲染完成后(浏览器绘制之后)
- 特长:优雅地处理副作用(数据获取、订阅等)
- 口头禅:"让子弹飞一会儿"
useLayoutEffect(闪电侠)
- 出场时机:DOM更新后但浏览器绘制前
- 必杀技:冻结时间修改DOM
- 座右铭:"我比光还快!"
这就像你在厨房做三明治:
useEffect
是先把三明治端上桌,再调整摆盘useLayoutEffect
是在端上桌前就调整好摆盘
感觉useLayoutEffect
很像DOMcontentloaded
,是DOM更新后执行
实战:
- 高度测量之争
javascript
function App() {
const boxRef = useRef()
useEffect(() => {
console.log('useEffect height', boxRef.current.offsetHeight)
}, [])
useLayoutEffect(() => {
console.log('useLayoutEffect height', boxRef.current.offsetHeight)
}, [])
return <div ref={boxRef} style={{ height: 100 }}></div>
}
运行这段代码,控制台会显示:
arduino
useLayoutEffect height 100
useEffect height 100
虽然结果相同,但顺序暴露了真相!
useLayoutEffect
就像百米赛跑的博尔特,总能在浏览器绘制前抢先执行。而 useEffect
则是悠闲的观光客,等页面渲染完才慢悠悠登场。
- 闪烁大作战:文字变形记
csharp
function App() {
const [content, setContent] = useState("曾经有一份真诚的爱情...")
const ref = useRef()
useLayoutEffect(() => {
setContent("React并不会立即更新状态...")
ref.current.style.height = '200px'
}, [])
return (
<div ref={ref} style={{ height: '50px', background: 'lightblue' }}>
{content}
</div>
)
}
这里发生了什么魔法?
-
初始状态:50px高度 + 星爷经典台词
-
useLayoutEffect
闪电出击:- 更换为技术说明文本
- 调整高度为200px
关键点:所有操作在浏览器绘制前完成!用户只看到最终效果,完全看不到中间状态。
如果换成 useEffect
,用户会先看到矮胖的星爷台词,然后突然变成高瘦的技术文本------这就是恼人的"闪烁"效果!
可以看到下述效果,我们是很明显的可以看到闪烁效果,其实就是因为useEffect
发生在渲染后,从无到有,给人一种闪烁感,而useLayoutEffect
是在DOM构建之后,阻塞页面渲染,避免了这种情况

比喻:
useLayoutEffect
像魔术师的暗箱操作,在幕布拉起前完成所有准备;而useEffect
则是在观众注视下换道具,难免露馅。
- 弹窗居中:几何学魔法
csharp
function Modal() {
const ref = useRef()
useLayoutEffect(() => {
const height = ref.current.offsetHeight
ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`
}, [])
return (
<div ref={ref} style={{
position: 'absolute',
height: '200px',
width: '200px',
background: 'red'
}}>
我是弹窗
</div>
)
}
这个弹窗为何如此优雅?秘密在于:
- 先让弹窗以默认位置渲染(但此时位置不对)
useLayoutEffect
瞬间测量弹窗高度- 计算并设置垂直居中
整个过程快到用户无法察觉!就像超人换装------你永远只看到他穿着整齐的样子。
我们为什么要这么做呢?大家都知道重绘重排会影响性能,而const height = ref.current.offsetHeight
和ref.current.style.marginTop = ${(window.innerHeight - height) / 2}px
是会引起重绘重排的,那我们为什么不等它们在页面渲染前就完成工作,就不需要重绘了,这时候我们的 useLayoutEffect
就起了大作用,DOM结构构建完成,就进行工作,解决了烦恼
- 原理深潜:React 渲染流水线
理解执行时机是掌握这对兄弟的关键:
markdown
1. 组件渲染(生成虚拟DOM)
2. DOM更新(应用变更到真实DOM)
3. 🚨 useLayoutEffect 执行 🚨
4. 浏览器绘制(像素上屏)
5. useEffect 执行
当你在 useLayoutEffect
中修改 DOM 时:
- React 会中断浏览器绘制流程
- 重新回到步骤2(DOM更新)
- 形成神奇的"时间循环"
类比:这就像导演在电影上映前最后一刻修改镜头,观众看到的始终是最终版本。
- 性能警示:闪电侠的代价
能力越大,责任越大。useLayoutEffect
的同步特性是把双刃剑:
scss
// 危险操作!可能导致界面冻结
useLayoutEffect(() => {
// 复杂计算或大数据处理...
}, [])
黄金法则:
- 当需要操作 DOM 避免视觉闪烁时 → 选
useLayoutEffect
- 数据获取、日志记录等异步操作 → 用
useEffect
- 不确定时 → 先用
useEffect
,遇到闪烁再考虑切换
因为useLayoutEffect
会阻塞页面渲染,如果回调函数是执行复杂的逻辑,会很大程度阻塞页面渲染,极大地影响用户的体验,最终换来的肯定是用户的 X
趣味实验:肉眼可见的差异
让我们制造一个可控闪烁(请在安全环境下尝试):
javascript
function FlashDemo() {
const ref = useRef()
const [style, setStyle] = useState({ background: 'blue' })
useEffect(() => { // 换成 useLayoutEffect 观察变化
setTimeout(() => {
setStyle({ background: 'red', height: '200px' })
}, 1000)
}, [])
return <div ref={ref} style={style}>变色龙区块</div>
}
用 useEffect
时你会看到:
- 蓝色方块显示1秒
- 突然变成红色并长高

换成 useLayoutEffect
后:
- 页面保持空白1秒(阻塞渲染!)
- 直接显示最终的红色高方块
这个实验完美展示了二者的本质区别!
哲学思考:时间管理大师
React 团队设计这两个钩子,本质上是在解决时机选择这个古老哲学问题:
useEffect
代表"后见之明"(渲染后再调整)useLayoutEffect
代表"未雨绸缪"(渲染前搞定一切)
就像建筑工地的两种监工:
useEffect
:"楼盖好了我们再调整门窗位置"useLayoutEffect
:"在砌墙时就把门窗安到正确位置"
终极对决:何时召唤谁?
场景 | 推荐选手 | 原因 |
---|---|---|
DOM 测量/布局调整 | useLayoutEffect | 避免布局跳动和闪烁 |
数据获取 | useEffect | 不阻塞渲染,更好的性能 |
动画初始化 | useLayoutEffect | 确保动画开始时元素已在正确位置 |
事件监听/订阅 | useEffect | 清理函数更安全 |
第三方 DOM 库集成 | useLayoutEffect | 确保库初始化时 DOM 已更新 |
结语:掌握时间的大师
经过这次探险,我们终于理解了:
useEffect
是优雅的异步诗人useLayoutEffect
是精准的同步外科医生
记住这个魔法口诀:
"闪烁用 Layout,异步用 Effect"
下次当你看到页面元素跳舞时,别急着怪显示器------可能是你选错了特效钩子!现在就去给你的组件们安排正确的时机管理者吧,它们会回报你丝般顺滑的用户体验!