Appearance
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。