Skip to content

useEffect 的执行时机具体是什么时候?它和 useLayoutEffect 有什么核心区别?

useEffectuseLayoutEffect:时机之差

在 React 中,useEffectuseLayoutEffect 都用于处理副作用(side effects),例如数据获取、DOM 操作、订阅等。它们共享相同的 API,但执行时机却截然不同,这导致了它们在应用场景上的本质区别。

useEffect:异步执行,不阻塞浏览器渲染

我们最常用的 useEffect异步执行的。在一个典型的渲染流程中,React 会完成以下步骤:

  1. 触发渲染:组件的 stateprops 发生变化。

  2. 渲染阶段 (Render Phase):React 调用组件的 render 方法(或函数组件本身),计算出新的虚拟 DOM (Virtual DOM)。

  3. 提交阶段 (Commit Phase):React 将计算出的变更应用到真实的 DOM 上。

  4. 浏览器绘制 (Browser Paint):浏览器根据更新后的 DOM 树,重新绘制界面,将像素呈现在屏幕上。

  5. 执行 useEffect:在浏览器完成绘制之后**,useEffect 内部的回调函数才会被异步调用。

这种设计的最大优势在于性能。由于 useEffect 的执行不会阻塞浏览器的绘制过程,用户的界面响应会更快。即使用于副作用的函数需要一些时间来执行(例如,一个网络请求),用户也能先看到更新后的 UI,从而获得更流畅的体验。

我们可以将这个过程想象成:

房间已经打扫干净,家具也摆放好了(DOM 更新),客人已经可以看到整洁的房间了(浏览器绘制)。然后,我们再异步地去处理一些收尾工作,比如给花瓶换水(执行 useEffect)。

useLayoutEffect:同步执行,阻塞浏览器渲染

useEffect 不同,useLayoutEffect同步执行的。它的执行时机插入到了 DOM 更新和浏览器绘制之间:

  1. 触发渲染:同上。

  2. 渲染阶段 (Render Phase):同上。

  3. 提交阶段 (Commit Phase):React 将变更应用到真实的 DOM 上。

  4. 执行 useLayoutEffect**:在 DOM 更新完毕后,浏览器绘制之前useLayoutEffect 内部的回调函数会立即、同步地被调用。

  5. 浏览器绘制 (Browser Paint):在 useLayoutEffect 的代码执行完毕后,浏览器才会进行绘制。

这种同步阻塞的特性是一把双刃剑。

  • 优点:它允许我们在浏览器向用户展示更新之前,对 DOM 进行最后的修改或测量。这对于避免“闪烁”(flickering)至关重要。例如,如果我们需要在渲染后立即获取一个元素的尺寸并据此调整其样式,使用 useLayoutEffect 可以确保用户不会先看到一个未调整的中间状态。

  • 缺点:如果 useLayoutEffect 内部执行了耗时很长的同步代码,它会阻塞浏览器的绘制过程,导致页面卡顿,让用户感觉应用响应迟钝。

我们可以将这个过程想象成:

房间已经打扫干净,家具也摆放好了(DOM 更新)。在邀请客人进来参观(浏览器绘制)之前,我们立即、同步地调整一下画框的位置,确保它绝对水平(执行 useLayoutEffect)。整个过程完成后,才让客人看到最终完美的布局。

核心区别总结

特性useEffectuseLayoutEffect
执行时机在浏览器绘制之后在浏览器绘制之前
执行方式异步 (Asynchronous)同步 (Synchronous)
对渲染的影响不会阻塞浏览器绘制会阻塞浏览器绘制
性能更高,不影响页面响应较低,可能导致卡顿
适用场景大多数副作用,如数据获取、事件监听、订阅等需要在浏览器绘制前读取/修改 DOM 布局,以避免视觉闪烁

一个典型的“闪烁”场景

设想一个场景:我们需要在组件渲染后,将一个 divwidth 设置为 100px

如果使用 useEffect,可能会发生以下情况:

  1. 组件渲染,div 以其初始样式(比如 width: 0)被渲染到 DOM 中。

  2. 浏览器绘制,用户在极短的时间内看到了一个宽度为 0div

  3. useEffect 执行,通过 style.width = '100px' 将其宽度修改。

  4. 浏览器再次绘制,div 的宽度变为 100px

这个从 0100px 的过程,即使非常快,也可能导致一次肉眼可见的“闪烁”。

而如果使用 useLayoutEffect

  1. 组件渲染,div 以其初始样式被更新到 DOM 中。

  2. useLayoutEffect 同步执行**,在浏览器绘制之前,就将 divstyle.width 设置为 100px

  3. 浏览器进行首次绘制时,它看到的就是一个宽度已经是 100pxdiv

因此,用户自始至终只会看到最终的、正确的状态,闪烁问题得以避免。

我们应该如何选择?

遵循一个简单的原则:

  • 始终优先使用 useEffect**。它是处理绝大多数副作用的默认和最佳选择,因为它不会损害应用的性能和用户体验。

  • 仅在 useEffect 无法解决问题时,才考虑 useLayoutEffect**。这个“问题”通常是指,在操作 DOM 后出现了视觉上的不一致或闪烁。典型的场景包括:

    • 在渲染后需要测量 DOM 元素(如获取尺寸、位置)并根据结果同步更新样式或布局
    • 处理与第三方 DOM 库的集成,需要在绘制前进行精确的 DOM 操作。
    • 实现复杂的动画,需要在绘制前计算初始和结束状态。

总之,useLayoutEffect 是一个强大的工具,但也是一把锋利的刀。只有在明确知道为什么需要它,并且了解其性能代价时,才应该使用它。在日常开发中,我们 99% 的场景都应该,也只需要使用 useEffect

不知道说啥了很无语了