精读React hook(六):useLayoutEffect解决了什么问题?

如果你会用useEffect精读React hook(五):useEffect使用细节知多少?),那么你一定也会用useLayoutEffect。因为它们的用法一模一样。

useEffectuseLayoutEffect的区别仅有一个:

  • useEffect执行时机是在React的渲染和提交阶段之后;
  • useLayoutEffect执行时机是在React的提交阶段之后,但在浏览器实际绘制屏幕之前。

useEffect vs useLayoutEffect

我们通过一个例子来看看阻塞和非阻塞对用户来说有什么区别。

jsx 复制代码
    import React, { useEffect, useLayoutEffect, useState, useRef } from 'react';

    function BoxComparison() {
      const [heightEffect, setHeightEffect] = useState(0);
      const [heightLayoutEffect, setHeightLayoutEffect] = useState(0);
      const refEffect = useRef(null);
      const refLayoutEffect = useRef(null);

      useEffect(() => {
        if (refEffect.current) {
          setHeightEffect(refEffect.current.offsetWidth);
        }
      }, []);

      useLayoutEffect(() => {
        if (refLayoutEffect.current) {
          setHeightLayoutEffect(refLayoutEffect.current.offsetWidth);
        }
      }, []);

      return (
        <div>
          <div>
            <h2>使用 useEffect</h2>
            <div ref={refEffect} style={{ width: '200px', height: '50px', background: 'lightgray' }}>这是一个方块</div>
            <div style={{ width: '100px', height: `${heightEffect}px`, background: 'red', marginTop: '10px' }}>红色方块</div>
          </div>
          
          <div style={{ marginTop: '30px' }}>
            <h2>使用 useLayoutEffect</h2>
            <div ref={refLayoutEffect} style={{ width: '200px', height: '50px', background: 'lightgray' }}>这是一个方块</div>
            <div style={{ width: '100px', height: `${heightLayoutEffect}px`, background: 'blue', marginTop: '10px' }}>蓝色方块</div>
          </div>
        </div>
      );
    }

    export default BoxComparison;

这个例子写了两个方块,分别使用useEffectuseLayoutEffect来更新高度,代码实际效果在我的演示站查看。

当你在性能较差的设备上肉眼可以明显看到区别:

  • useEffect的方块会闪一下
  • useLayoutEffect的方块则不会闪

如果你的电脑性能比较好,可以尝试多次刷新,也有一定几率看到useEffect的闪动。

我们应该这样描述二者的区别:

  • useEffect: 执行时机是在React的渲染和提交阶段之后。这意味着当任何相关DOM更改被应用并且组件已被重新渲染后,useEffect里的代码会执行。但它是异步的,所以可能会在浏览器的下一个绘制周期之后才执行。
  • useLayoutEffect: 执行时机是在React的提交阶段之后,但在浏览器实际绘制屏幕之前。这使得你可以同步地读取或更改DOM,然后让浏览器在下一次绘制时立即体现这些更改,从而避免不必要的闪烁或布局跳动。

useLayoutEffect的作用

我们已经清楚了useLayoutEffect的特性了,那么可以猜想,useLayoutEffect是作用于这样的场景:需要在浏览器绘制前获取 DOM 元素的大小或位置,或者在浏览器绘制前修改 DOM。

这里有一个非常典型的场景------tooltip 组件。我们就来写一个 tooltip 组件,应用useLayoutEffect来自适应设置 tooltip 位置。

我们的需求是:鼠标移入一个按钮,能够判断 tooltip 展示区域,如果按钮上方空间足够,则显示在上方,如果按钮上方空间不够,则自适应显示在按钮下方。

为了保证没有页面抖动,我们要使用useLayoutEffect来更新显示的位置,示例代码如下:

jsx 复制代码
    import React, { useLayoutEffect, useRef, useState } from "react";
    import { createPortal } from "react-dom";

    export default function HoverTooltip() {
      const containerRef = useRef(null);

      return (
        <div
          ref={containerRef}
          className="p-8 bg-gray-100 w-full rounded-xl mt-5 shadow-lg m-4 space-y-4 overflow-hidden"
        >
          <ButtonWithTooltip
            containerRef={containerRef}
            tooltipContent="This tooltip does not fit above the button. This is why it's displayed below instead!"
          >
            Hover over me (tooltip above)
          </ButtonWithTooltip>
          <ButtonWithTooltip
            containerRef={containerRef}
            tooltipContent="This tooltip fits above the button"
          >
            Hover over me (tooltip below)
          </ButtonWithTooltip>
          <ButtonWithTooltip
            containerRef={containerRef}
            tooltipContent="This tooltip fits above the button"
          >
            Hover over me (tooltip below)
          </ButtonWithTooltip>
        </div>
      );
    }

    const ButtonWithTooltip = ({ tooltipContent, containerRef, children }) => {
      const [targetRect, setTargetRect] = useState(null);
      const [containerRect, setContainerRect] = useState(null);
      const buttonRef = useRef(null);

      return (
        <div className="relative">
          <button
            ref={buttonRef}
            className="py-2 px-4 bg-blue-500 text-white rounded hover:bg-blue-600 active:bg-blue-700 focus:outline-none transition"
            onMouseEnter={() => {
              buttonRef.current &&
                setTargetRect(buttonRef.current.getBoundingClientRect());
              containerRef.current &&
                setContainerRect(containerRef.current.getBoundingClientRect());
            }}
            onMouseLeave={() => setTargetRect(null)}
          >
            {children}
          </button>
          {targetRect && containerRect && (
            <Tooltip targetRect={targetRect} containerRect={containerRect}>
              {tooltipContent}
            </Tooltip>
          )}
        </div>
      );
    };

    const Tooltip = ({ children, targetRect, containerRect }) => {
      const ref = useRef(null);
      const [tooltipHeight, setTooltipHeight] = useState(0);

      useLayoutEffect(() => {
        if (ref.current) {
          const { height } = ref.current.getBoundingClientRect();
          setTooltipHeight(height); // 设置高度
        }
      }, [children]);

      let tooltipX = targetRect.left;
      let tooltipY =
        targetRect.top - containerRect.top - tooltipHeight < 0
          ? targetRect.bottom
          : targetRect.top - tooltipHeight; // 计算位置

      return createPortal(
        <div
          ref={ref}
          className="absolute bg-gray-700 text-white py-1 px-2 rounded shadow-md"
          style={{
            left: `${tooltipX}px`,
            top: `${tooltipY}px`,
          }}
        >
          {children}
        </div>,
        document.body
      );
    };

这里示例中,我们写了三个按钮,每次鼠标移入按钮的时候,计算按钮到父级上沿的空间是否可以容纳一个 tooltip,如果足够,tooltip 就在按钮上方展示,如果不够,则在按钮下方展示。实际效果如图:

你也可以到演示站试一试。

总结

最后,我们再明确一下useEffectuseLayoutEffect分别在何时使用、useLayoutEffect的使用注意事项。

何时使用useEffect

  • 副作用与DOM无关:例如,数据获取、设置订阅、手动更改浏览器的URL等。
  • 不需要立即同步读取或更改DOM :如果你不关心可能的微小布局跳动或闪烁,那么useEffect就足够了。
  • 性能考虑useEffect通常对性能影响较小,因为它不会阻塞浏览器渲染。

何时使用useLayoutEffect

  • 需要同步读取或更改DOM:例如,你需要读取元素的大小或位置并在渲染前进行调整。
  • 防止闪烁 :在某些情况下,异步的useEffect可能会导致可见的布局跳动或闪烁。例如,动画的启动或某些可见的快速DOM更改。
  • 模拟生命周期方法 :如果你正在将旧的类组件迁移到功能组件,并需要模拟 componentDidMountcomponentDidUpdatecomponentWillUnmount的同步行为。

使用注意事项

  • 避免过度使用useLayoutEffect,因为它是同步的,可能会影响应用的性能。只有当你确实需要同步的DOM操作时才使用它。
  • 如果代码在服务器端渲染(SSR)中出现问题,考虑回退到useEffectuseLayoutEffect在服务器端渲染时不会运行,可能会引发警告或错误。

系列文章列表

精读React hook(一):useState 的几个基础用法和进阶技巧

精读React hook(二):React状态管理的强大工具------useReducer

精读React hook(三):useContext从基础应用到性能优化

精读React hook(四):useRef的多维用途

精读React hook(五):useEffect使用细节知多少?

精读React hook(六):useLayoutEffect解决了什么问题?

未完待续......

相关推荐
码事漫谈10 分钟前
解决 Anki 启动器下载错误的完整指南
前端
im_AMBER29 分钟前
Web 开发 27
前端·javascript·笔记·后端·学习·web
蓝胖子的多啦A梦1 小时前
低版本Chrome导致弹框无法滚动的解决方案
前端·css·html·chrome浏览器·版本不同造成问题·弹框页面无法滚动
玩代码1 小时前
vue项目安装chromedriver超时解决办法
前端·javascript·vue.js
訾博ZiBo1 小时前
React 状态管理中的循环更新陷阱与解决方案
前端
StarPrayers.1 小时前
旅行商问题(TSP)(2)(heuristics.py)(TSP 的两种贪心启发式算法实现)
前端·人工智能·python·算法·pycharm·启发式算法
一壶浊酒..2 小时前
ajax局部更新
前端·ajax·okhttp
DoraBigHead3 小时前
React 架构重生记:从递归地狱到时间切片
前端·javascript·react.js
彩旗工作室3 小时前
WordPress 本地开发环境完全指南:从零开始理解 Local by Flywhee
前端·wordpress·网站
iuuia3 小时前
02--CSS基础
前端·css