useEffect 的执行时机具体是什么时候?它和 useLayoutEffect 有什么核心区别?
useEffect 与 useLayoutEffect:时机之差
在 React 中,useEffect 和 useLayoutEffect 都用于处理副作用(side effects),例如数据获取、DOM 操作、订阅等。它们共享相同的 API,但执行时机却截然不同,这导致了它们在应用场景上的本质区别。
useEffect:异步执行,不阻塞浏览器渲染
我们最常用的 useEffect 是异步执行的。在一个典型的渲染流程中,React 会完成以下步骤:
触发渲染:组件的
state或props发生变化。渲染阶段 (Render Phase):React 调用组件的 render 方法(或函数组件本身),计算出新的虚拟 DOM (Virtual DOM)。
提交阶段 (Commit Phase):React 将计算出的变更应用到真实的 DOM 上。
浏览器绘制 (Browser Paint):浏览器根据更新后的 DOM 树,重新绘制界面,将像素呈现在屏幕上。
执行
useEffect:在浏览器完成绘制之后**,useEffect内部的回调函数才会被异步调用。
这种设计的最大优势在于性能。由于 useEffect 的执行不会阻塞浏览器的绘制过程,用户的界面响应会更快。即使用于副作用的函数需要一些时间来执行(例如,一个网络请求),用户也能先看到更新后的 UI,从而获得更流畅的体验。
我们可以将这个过程想象成:
房间已经打扫干净,家具也摆放好了(DOM 更新),客人已经可以看到整洁的房间了(浏览器绘制)。然后,我们再异步地去处理一些收尾工作,比如给花瓶换水(执行 useEffect)。
useLayoutEffect:同步执行,阻塞浏览器渲染
与 useEffect 不同,useLayoutEffect 是同步执行的。它的执行时机插入到了 DOM 更新和浏览器绘制之间:
触发渲染:同上。渲染阶段 (Render Phase):同上。提交阶段 (Commit Phase):React 将变更应用到真实的 DOM 上。执行
useLayoutEffect**:在 DOM 更新完毕后,浏览器绘制之前,useLayoutEffect内部的回调函数会立即、同步地被调用。浏览器绘制 (Browser Paint):在useLayoutEffect的代码执行完毕后,浏览器才会进行绘制。
这种同步阻塞的特性是一把双刃剑。
优点:它允许我们在浏览器向用户展示更新之前,对 DOM 进行最后的修改或测量。这对于避免“闪烁”(flickering)至关重要。例如,如果我们需要在渲染后立即获取一个元素的尺寸并据此调整其样式,使用
useLayoutEffect可以确保用户不会先看到一个未调整的中间状态。缺点:如果
useLayoutEffect内部执行了耗时很长的同步代码,它会阻塞浏览器的绘制过程,导致页面卡顿,让用户感觉应用响应迟钝。
我们可以将这个过程想象成:
房间已经打扫干净,家具也摆放好了(DOM 更新)。在邀请客人进来参观(浏览器绘制)之前,我们立即、同步地调整一下画框的位置,确保它绝对水平(执行 useLayoutEffect)。整个过程完成后,才让客人看到最终完美的布局。
核心区别总结
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 在浏览器绘制之后 | 在浏览器绘制之前 |
| 执行方式 | 异步 (Asynchronous) | 同步 (Synchronous) |
| 对渲染的影响 | 不会阻塞浏览器绘制 | 会阻塞浏览器绘制 |
| 性能 | 更高,不影响页面响应 | 较低,可能导致卡顿 |
| 适用场景 | 大多数副作用,如数据获取、事件监听、订阅等 | 需要在浏览器绘制前读取/修改 DOM 布局,以避免视觉闪烁 |
一个典型的“闪烁”场景
设想一个场景:我们需要在组件渲染后,将一个 div 的 width 设置为 100px。
如果使用 useEffect,可能会发生以下情况:
组件渲染,
div以其初始样式(比如width: 0)被渲染到 DOM 中。浏览器绘制,用户在极短的时间内看到了一个宽度为
0的div。useEffect执行,通过style.width = '100px'将其宽度修改。浏览器再次绘制,
div的宽度变为100px。
这个从 0 到 100px 的过程,即使非常快,也可能导致一次肉眼可见的“闪烁”。
而如果使用 useLayoutEffect:
组件渲染,
div以其初始样式被更新到 DOM 中。useLayoutEffect同步执行**,在浏览器绘制之前,就将div的style.width设置为100px。浏览器进行首次绘制时,它看到的就是一个宽度已经是
100px的div。
因此,用户自始至终只会看到最终的、正确的状态,闪烁问题得以避免。
我们应该如何选择?
遵循一个简单的原则:
始终优先使用 useEffect**。它是处理绝大多数副作用的默认和最佳选择,因为它不会损害应用的性能和用户体验。
仅在 useEffect 无法解决问题时,才考虑useLayoutEffect**。这个“问题”通常是指,在操作 DOM 后出现了视觉上的不一致或闪烁。典型的场景包括:- 在渲染后需要测量 DOM 元素(如获取尺寸、位置)并根据结果同步更新样式或布局。
- 处理与第三方 DOM 库的集成,需要在绘制前进行精确的 DOM 操作。
- 实现复杂的动画,需要在绘制前计算初始和结束状态。
总之,useLayoutEffect 是一个强大的工具,但也是一把锋利的刀。只有在明确知道为什么需要它,并且了解其性能代价时,才应该使用它。在日常开发中,我们 99% 的场景都应该,也只需要使用 useEffect。
