useEffect 和 useLayoutEffect

React Hooks 深度解析:useEffectuseLayoutEffect

在 React Hooks 中,useEffect 是我们最常用的副作用 Hook,用于处理组件渲染后的各种操作,比如数据请求、订阅事件、DOM 操作等。然而,React 还提供了一个鲜为人知的兄弟 Hook:useLayoutEffect。虽然它们的名字相似,但执行时机却大相径庭,理解它们之间的区别对于编写高性能和无视觉闪烁的 React 应用至关重要。


useEffect: 最常用的副作用 Hook

useEffect 处理组件渲染后的各种操作,例如数据请求、订阅事件、DOM 操作等。

useEffect 的执行时机

useEffect 的回调函数是在 浏览器完成 DOM 更新和屏幕绘制之后 才异步执行的。这种异步执行的特性使得 useEffect 不会阻塞浏览器的渲染进程,从而保证了应用的响应性。

useEffect 基本用法

useEffect 接收两个参数:

  1. 一个回调函数(effect 函数) :这是你实际实现副作用的地方。
  2. 一个可选的依赖项数组(dependency array) :这个数组控制着回调函数的执行时机。

JavaScript

scss 复制代码
useEffect(() => {
  // 副作用代码
  // 例如:数据获取、事件监听、DOM 操作

  return () => {
    // 可选的清理函数
    // 在组件卸载或回调函数重新执行前运行
  };
}, [d1, d2]); // 依赖项数组

依赖项数组控制回调函数执行时机:

  1. 没有依赖项数组:每次组件渲染完成后都执行。
  2. 空数组 [] :只在组件首次挂载时执行一次。
  3. 包含依赖项的数组 [dep1, dep2, ...] :当依赖项数组中包含一个或多个变量时,函数会在组件首次挂载时执行一次 ,并且在数组中的任何一个依赖项发生变化时重新执行

清理函数:告别内存泄漏

useEffect 的回调函数可以选择性地返回一个函数 。这个返回的函数就是清理函数(cleanup function)

清理函数会在以下两种情况下执行:

  1. 组件卸载时 :当组件从 DOM 中被移除时,清理函数会执行,以清除 effect 订阅的资源。
  2. 下一次 effect 执行前 :如果 effect 会因为依赖项的变化而重新运行,那么在执行新的 effect 之前,上一次 effect 返回的清理函数会先执行。这确保了在新的订阅或操作开始之前,旧的资源已被正确清理。

useLayoutEffect

useLayoutEffect 是一个与 useEffect 签名相同 的 Hook,但它在 DOM 更新之后,浏览器执行绘制 (Paint) 之前同步执行

简单来说:

  • useEffect渲染内容绘制到屏幕之后 异步执行。
  • useLayoutEffectDOM 更新完成后,但浏览器还没来得及绘制到屏幕上 同步执行。

useLayoutEffect 的执行时机

为了更好地理解 useLayoutEffect,我们来对比一下 React 组件生命周期中相关 Hook 的执行顺序:

  1. React 更新 DOM:这是 React 计算出新的 DOM 树并将其应用到实际浏览器 DOM 的阶段。
  2. useLayoutEffect 回调执行 :此时 DOM 已经更新完毕,但屏幕还没有重新绘制。useLayoutEffect 的回调函数会在这里同步执行。如果你的操作会直接影响到布局或需要读取 DOM 元素的精确尺寸,那么这里是最好的时机。
  3. 浏览器绘制 (Paint) :浏览器将更新后的 DOM 内容渲染到屏幕上。
  4. useEffect 回调执行 :在浏览器完成绘制之后,useEffect 的回调才会被异步执行。

这个同步执行的特性,意味着 useLayoutEffect 的回调函数会阻塞浏览器绘制 。如果 useLayoutEffect 中的操作耗时过长,可能会导致用户看到明显的卡顿或页面闪烁。


什么时候使用 useLayoutEffect

useLayoutEffect 的主要应用场景是当你需要在 DOM 更新后立即读取布局信息同步修改 DOM 样式以避免视觉上的闪烁时。

以下是几个常见的应用场景:

  1. 测量 DOM 元素尺寸或位置并立即基于此进行布局调整: 例如,你需要在组件渲染后获取一个元素的宽度,然后根据这个宽度调整另一个元素的位置,以确保它们精确对齐。如果使用 useEffect,可能会出现元素先以旧位置显示,然后瞬间跳到新位置的"闪烁"现象。

    JavaScript

    javascript 复制代码
    import React, { useRef, useLayoutEffect, useState } from 'react';
    
    function Tooltip({ children, text }) {
      const targetRef = useRef(null);
      const tooltipRef = useRef(null);
      const [tooltipStyle, setTooltipStyle] = useState({});
    
      useLayoutEffect(() => {
        if (targetRef.current && tooltipRef.current) {
          const targetRect = targetRef.current.getBoundingClientRect();
          const tooltipRect = tooltipRef.current.getBoundingClientRect();
    
          // 计算 Tooltip 的位置,使其居中显示在目标元素上方
          // 并向上偏移一些,确保在 target 元素上方可见
          setTooltipStyle({
            left: targetRect.left + targetRect.width / 2 - tooltipRect.width / 2,
            top: targetRect.top - tooltipRect.height - 10,
            position: 'absolute',
          });
        }
      }, [children]); // 依赖 children 变化时重新计算
    
      return (
        <span ref={targetRef} style={{ position: 'relative', display: 'inline-block' }}>
          {children}
          <div ref={tooltipRef} style={{ ...tooltipStyle, background: 'black', color: 'white', padding: '5px', borderRadius: '3px' }}>
            {text}
          </div>
        </span>
      );
    }
    
    function App() {
      const [showMore, setShowMore] = useState(false);
    
      return (
        <div style={{ padding: '100px', display: 'flex', flexDirection: 'column', gap: '20px' }}>
          <Tooltip text="这是一个提示">悬停在我上面</Tooltip>
          <p>
            这是一个很长的段落,用于演示当内容动态变化时,`useLayoutEffect` 如何避免闪烁。
            {showMore && (
              <span>
                <br />
                更多内容在这里展开。如果这里的内容长度发生变化,Tooltip 的位置可能需要重新计算。
                使用 `useLayoutEffect` 可以确保 Tooltip 在新布局绘制到屏幕之前就处于正确位置。
              </span>
            )}
          </p>
          <button onClick={() => setShowMore(!showMore)}>
            {showMore ? '收起' : '展开更多'}
          </button>
        </div>
      );
    }

    避免页面"闪烁"的例子:

    在上面的 Tooltip 组件示例中,我们使用 useLayoutEffect 来精确计算并设置 Tooltip 的位置。想象一下,如果 children(例如"悬停在我上面"的文本)的宽度动态改变,或者父组件的布局发生变化导致 targetRef 元素的位置改变了。

    • 如果使用 useEffect

      1. React 更新 DOM,targetRef 元素的新位置或尺寸被计算。
      2. 浏览器绘制屏幕,用户可能短暂地看到 Tooltip 还在旧位置。
      3. useEffect 回调执行,计算出 Tooltip 的新位置并更新 tooltipStyle
      4. React 再次渲染,Tooltip 跳到新位置。 这种 "先显示旧位置,再跳到新位置" 的过程就会导致视觉上的**"闪烁"**。
    • 使用 useLayoutEffect

      1. React 更新 DOM,targetRef 元素的新位置或尺寸被计算。
      2. useLayoutEffect 回调立即同步执行 ,它能够读取到 targetRef 的最新准确位置和尺寸。
      3. setTooltipStyle 同步更新状态,导致 React 立即安排新的渲染。
      4. 浏览器进行绘制时,Tooltip 已经根据最新的计算结果处于正确的位置,用户从一开始就看到 Tooltip 在正确的位置,从而避免了任何视觉上的"闪烁"

什么时候 使用 useLayoutEffect

由于 useLayoutEffect 是同步执行并阻塞浏览器绘制的,因此应该谨慎使用

  • 大多数情况下,你应该优先使用 useEffect 如果你的副作用操作不需要在 DOM 绘制前立即执行,或者不涉及读取 DOM 布局信息,那么 useEffect 是更好的选择,因为它不会阻塞浏览器绘制,从而避免影响用户体验。
  • 避免在 useLayoutEffect 中执行耗时操作。 任何长时间运行的同步操作都会导致页面卡顿。如果你的操作耗时,考虑将其放到 useEffect 中,或者使用 requestAnimationFrame 进行优化。
  • 不涉及 DOM 操作或布局读取的操作: 例如,数据请求、设置订阅、日志记录等,这些都应该放在 useEffect 中。

useLayoutEffectuseEffect 的对比总结

特性 useEffect useLayoutEffect
执行时机 DOM 更新并绘制到屏幕后 (异步) DOM 更新后,绘制到屏幕前 (同步)
是否阻塞绘制
可见性闪烁 可能导致 (如果依赖于布局并需立即更新) 避免 (在绘制前完成所有布局调整)
优先级 推荐在大多数副作用场景使用 仅在需要同步读取/修改布局时使用
性能影响 较低 (不阻塞主线程) 较高 (可能阻塞主线程,导致卡顿)
相关推荐
西瓜_号码2 小时前
React中Redux基础和路由介绍
javascript·react.js·ecmascript
伟笑3 小时前
React 的常用钩子函数在Vue中是如何设计体现出来的。
前端·react.js
FogLetter4 小时前
React组件开发进阶:本地存储与自定义Hooks的艺术
前端·javascript·react.js
AliciaIr7 小时前
深入React事件机制:解密“合成事件”与“事件委托”的底层奥秘
javascript·react.js
心.c8 小时前
后台管理系统-权限管理
javascript·react.js·github
杨进军9 小时前
实现 React 函数组件渲染
前端·react.js·前端框架
归于尽9 小时前
被 50px 到 200px 的闪烁整破防了?useLayoutEffect 和 useEffect 的区别原来在这
前端·react.js
杨进军9 小时前
实现 React Fragment 节点渲染
前端·react.js·前端框架
杨进军9 小时前
实现 React 类组件渲染
前端·react.js·前端框架
FogLetter9 小时前
React组件开发之Todos基础:从零打造一个优雅的待办事项应用
前端·javascript·react.js