React中的useLayoutEffect:解决闪烁问题的利器

前言

作为一名前端开发者,你是否遇到过React组件渲染时画面闪烁的问题?明明代码逻辑没问题,却出现了短暂的视觉跳动?这很可能是因为你使用了useEffect而不是useLayoutEffect。今天,我将带你详细了解这两个Hook的区别,以及如何正确使用useLayoutEffect来提升用户体验。

useEffect vs useLayoutEffect

useEffect

useEffect是React中处理副作用的标准方式,其执行时机为:

  • 在渲染完成后异步执行
  • 组件更新时执行
  • 组件卸载前执行清理函数
jsx 复制代码
useEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数
  }
}, [依赖项]);

useLayoutEffect

useLayoutEffectuseEffect有着关键区别:

  • 在DOM更新之后同步触发
  • 会阻塞页面的渲染
  • 在浏览器绘制之前执行
jsx 复制代码
useLayoutEffect(() => {
  // DOM操作代码
  return () => {
    // 清理函数
  }
}, [依赖项]);

执行时机对比

两者的执行顺序如下:

  1. React更新DOM
  2. useLayoutEffect执行
  3. 浏览器绘制屏幕
  4. useEffect执行

为了更好地理解两者区别,我们来看一下它们的执行顺序:

text 复制代码
【React更新DOM】→【useLayoutEffect执行】→【浏览器绘制】→【useEffect执行】

这种执行顺序的差异导致了它们适用于不同的场景。

js 复制代码
function App() {
  // 响应式对象
  const boxRef = useRef();
  console.log(boxRef.current,boxRef);
  
  useEffect(()=>{
    console.log('useEffect height',boxRef.current.offsetHeight);
  },[])

  useLayoutEffect(()=>{
    console.log('useLayoutEffect height',boxRef.current.offsetHeight);
  },[])

可以看到LayoutEffect是在useEffect之前行。

useLayoutEffect解决的问题

防止闪烁

最常见的使用场景是解决组件"闪烁"问题。当你需要在DOM更新后立即进行样式计算或DOM操作,并且不希望用户看到中间状态时,useLayoutEffect是最佳选择。

同步获取DOM元素尺寸

当你需要在渲染后立即获取DOM元素的尺寸并基于此进行其他计算时,useLayoutEffect能确保你获取的是最新的DOM信息。

实际案例

让我们看一个简单例子,展示两者的区别:

jsx 复制代码
// 使用useEffect的版本
function FlickeringComponent() {
  const [width, setWidth] = useState(0);
  
  useEffect(() => {
    // 会导致闪烁,因为先渲染了width=0的状态
    setWidth(document.getElementById('myElement').getBoundingClientRect().width);
  }, []);
  
  return (
    <div id="myElement" style={{ width: width ? `${width}px` : '100%' }}>
      内容区域
    </div>
  );
}

// 使用useLayoutEffect的版本
function SmoothComponent() {
  const [width, setWidth] = useState(0);
  
  useLayoutEffect(() => {
    // 不会闪烁,因为在浏览器绘制前就更新了状态
    setWidth(document.getElementById('myElement').getBoundingClientRect().width);
  }, []);
  
  return (
    <div id="myElement" style={{ width: width ? `${width}px` : '100%' }}>
      内容区域
    </div>
  );
}

再来看以下代码来展示两个React hooks的执行时机区别:

js 复制代码
import {
  useState,
  useEffect,
  useLayoutEffect,
  useRef
} from 'react'
import './App.css'
import { use } from 'react'

function App() {
  // 响应式对象
  const boxRef = useRef();
  console.log(boxRef.current, boxRef);

  useEffect(() => {
    console.log('useEffect height', boxRef.current.offsetHeight);
  }, [])

  useLayoutEffect(() => {
    console.log('useLayoutEffect height', boxRef.current.offsetHeight);
  }, [])

  return (
    <>
      <div ref={boxRef} style={{ height: 100 }}></div>
    </>
  )
}

export default App

让我们看到打印的效果:可以看到useLayoutEffect会在浏览器绘制前同步执行,useEffect在浏览器绘制后异步执行,不阻塞渲染。

现在到了大家最期待的节目了,解决页面闪烁的问题,现在大家跟着我们一起来看到下面的代码。

js 复制代码
function Modal() {
  const ref = useRef();
  useEffect(() => {
    const height = ref.current.offsetHeight
    const width = ref.current.offsetWidth
    ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`
    ref.current.style.marginLeft = `${(window.innerWidth - width) / 2}px`
  }, [])

  return <div ref={ref} style={{ background: 'lightblue', position: 'absolute', height: '200px', width: '200px' }}>我是弹窗</div>
}

function App() {
  return (
    <>
      <Modal />
    </>
  )
}


export default App

可以看到使用useEffect页面的刷新效果:

js 复制代码
function Modal() {
  const ref = useRef();
  useLayoutEffect(() => {
    const height = ref.current.offsetHeight
    const width = ref.current.offsetWidth
    ref.current.style.marginTop = `${(window.innerHeight - height) / 2}px`
    ref.current.style.marginLeft = `${(window.innerWidth - width) / 2}px`
  }, [])

  return <div ref={ref} style={{ background: 'lightblue', position: 'absolute', height: '200px', width: '200px' }}>我是弹窗</div>
}

function App() {
  return (
    <>
      <Modal />
    </>
  )
}


export default App

现在让我们继续往下看使用了useLayoutEffect的页面刷新效果:

可以看到使用该hook成功解决了页面闪烁的问题,这样我们就大功告成了,使用户交互体验效果更好。

性能考虑

虽然useLayoutEffect能解决特定问题,但它是同步执行的,会阻塞浏览器绘制。如果在其中执行耗时操作,可能导致页面响应变慢。因此:

  • 仅在必要时使用useLayoutEffect
  • 保持内部逻辑简洁高效
  • 对于不涉及DOM测量或不会引起视觉闪烁的副作用,优先使用useEffect

最佳实践

  1. 默认使用useEffect :大多数情况下,异步的useEffect就足够了
  2. 视觉问题时考虑useLayoutEffect:出现闪烁或需要同步DOM测量时使用
  3. 避免复杂计算 :在useLayoutEffect中避免进行耗时计算
  4. 谨慎使用状态更新 :在useLayoutEffect中更新状态可能导致额外渲染

📊 useEffect vs useLayoutEffect 总结表

特性 useEffect useLayoutEffect
执行时机 渲染后异步执行 DOM更新后同步执行
是否阻塞渲染
适用场景 数据获取、订阅、定时器 DOM测量、防闪烁、视觉更新
性能影响 较小 可能导致页面卡顿
SSR兼容性 良好 会有警告

总结

useLayoutEffect是解决特定UI问题的有力工具,尤其是在处理需要同步DOM操作的场景。理解useEffectuseLayoutEffect的区别,能帮助你在合适的场景选择合适的工具,构建更流畅的React应用。

记住,虽然useLayoutEffect能解决闪烁问题,但它也可能带来性能问题。在开发中要权衡利弊,合理使用。

你是否在项目中遇到过需要使用useLayoutEffect的场景?欢迎在评论区分享你的经验!

相关推荐
JosieBook8 分钟前
【web应用】若依框架中,使用Echarts导出报表为PDF文件
前端·pdf·echarts
袁煦丞1 小时前
Photopea云端修图不求人!cpolar内网穿透实验室第641个成功挑战
前端·程序员·远程工作
yk-ddm1 小时前
JavaScript实现文件下载完整方案
前端·javascript·html
万少1 小时前
04-自然壁纸实战教程-搭建基本工程
前端·harmonyos·客户端
karl_hg1 小时前
Element Plus 自定义(动态)表单组件
前端·vue.js·element
南岸月明1 小时前
从焦虑到专注:副业半年后我才明白的3件事
前端
晓13131 小时前
JavaScript加强篇——第八章 高效渲染与正则表达式
开发语言·前端·javascript
南囝coding2 小时前
做付费社群,强烈建议大家做这件事!
前端·后端
我是若尘2 小时前
Axios 如何跨域携带 Cookie?
前端
子林super2 小时前
主从数据全量迁移到分片集群测试
前端