深入理解 useLayoutEffect:解决 UI "闪烁"问题的利器

前言

在 React 开发中,我们经常需要处理副作用(side effects)。React 提供了两个主要的 Hook 来处理副作用:useEffectuseLayoutEffect。虽然它们看起来很相似,但在使用时机和使用场景上有着重要区别。本文将重点介绍 useLayoutEffect,探讨它的工作原理、使用场景以及如何利用它解决常见的 UI 问题。

useEffect 回顾

在深入 useLayoutEffect 之前,让我们先快速回顾一下 useEffect

js 复制代码
useEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数
  };
}, [dependencies]);

可以看到,useEffect 的特点:

  • 在组件渲染完成后异步执行
  • 不会阻塞浏览器的绘制(paint)
  • 适用于大多数副作用场景(数据获取、订阅等)

useLayoutEffect 详解

API的用法

useLayoutEffect 的 API的用法如下:

js 复制代码
useLayoutEffect(() => {
  // 副作用代码
  return () => {
    // 清理函数
  };
}, [dependencies]);

你会发现,其API的使用几乎和useEffect是完全相同的

执行阶段与特性

对于useLayoutEffect理解其执行阶段和特性是最重要的

  1. 执行阶段

    • 在 DOM 更新之后
    • 在浏览器计算布局(Layout)之后
    • 在浏览器实际绘制(Paint)屏幕之前
  2. 特性

    • 同步执行,会阻塞浏览器的绘制
    • 适合需要同步读取或修改 DOM 的场景
    • 可以避免视觉上的"闪烁"或布局跳动

React的生命周期

理解 React 组件的生命周期有助于我们更好地把握这两个 Hook 的区别:

React的生命周期:

  1. 组件渲染(Render) - React 计算虚拟 DOM 变化
  2. DOM 更新 - React 将变化应用到真实 DOM
  3. 浏览器计算布局(Layout) - 浏览器计算元素的新位置和尺寸
  4. useLayoutEffect 执行 ← 在这里!
  5. 浏览器绘制(Paint) - 实际将像素绘制到屏幕
  6. useEffect 执行

典型使用场景

1. 避免布局"闪烁"

什么是布局闪烁?

布局闪烁是指当用户界面的元素在渲染后突然改变位置或尺寸,导致用户看到明显的跳动或闪烁效果。这种现象通常发生在 React 组件需要根据 DOM 元素的尺寸或位置进行动态调整时。

发生布局闪烁的原因是当我们使用传统的useEffect时,元素变化是在渲染之后进行的

看这个例子:

js 复制代码
function App(){
  const [content,setContent] = useState("11111111111111111111111111111111111111")
  
  //使用useEffect时:
  useEffect(() => {
    setContent('2222222222222222222222222222222222')
  },[])
  
  return (
    <div>{content}</div>
  )
}

当使用useEffect时:

  1. 组件首次渲染,content 初始值为 "1111..."(长字符串1)
  2. React 将 <div>1111...</div> 提交给浏览器绘制
  3. 用户短暂看到 "1111..." 的内容
  4. useEffect 回调执行,触发 setContent('2222...')
  5. 组件重新渲染,显示 <div>2222...</div>
  6. 用户看到内容从 "1111..." 变为 "2222..."(闪动过程)

现代浏览器和设备很高级,导致闪动的速度很快,你可能观察不到这个效果,但是在一些早期性能较低的设备中,对于页面中的一大堆内容,闪烁会给用户带来不好的用户体验

所以我们需要使用 useLayoutEffect ,这样做可以确保这些调整在用户看到屏幕之前完成,因此优化闪烁效果

js 复制代码
function App(){
  const [content,setContent] = useState("11111111111111111111111111111111111111")
  
  //使用useLayoutEffect时:
  useLayoutEffect(() => {
    setContent('2222222222222222222222222222222222')
  },[])
  
  return (
    <div>{content}</div>
  )
}

这里的过程是这样的:

  1. 组件首次渲染,content 初始值为 "1111..."
  2. 在浏览器绘制前,useLayoutEffect 回调执行,设置 content 为 "2222..."
  3. 组件立即重新渲染,准备显示 <div>2222...</div>
  4. 浏览器绘制最终结果
  5. 用户直接看到 "2222...",没有看到中间状态

这种过程你可以理解为useLayoutEffect是和渲染同步进行的,因此就不会出现闪烁效果!

2. 同步获取 DOM 属性

useLayoutEffect还有一种常见的应用场景就是,当需要同步读取 DOM 并立即基于读取的值进行更新时:

js 复制代码
function AutoHeightTextarea() {
  const textareaRef = useRef(null);
  const [height, setHeight] = useState('auto');
  
  useLayoutEffect(() => {
    // 同步获取滚动高度并设置
    setHeight(`${textareaRef.current.scrollHeight}px`);
  }, [value]);

  return (
    <textarea
      ref={textareaRef}
      style={{ height }}
      value={value}
      onChange={handleChange}
    />
  );
}

在这个例子中,为什么必须使用 useLayoutEffect?

  1. 同步性需求

    • 我们需要在浏览器绘制前确保文本框高度已经调整
    • 任何延迟都会导致用户看到文本框高度变化的"跳跃"效果
  2. DOM 测量与更新的原子性

性能考虑

由于 useLayoutEffect 会阻塞浏览器绘制,不当使用可能导致性能问题。遵循以下准则:

  1. 仅在必要时使用 - 大多数情况下,useEffect 是更好的选择
  2. 保持轻量 - 在 useLayoutEffect 中执行的操作应该尽可能快速
  3. 避免频繁触发 - 谨慎设置依赖项,避免不必要的执行

两者区别总结

特性 useEffect useLayoutEffect
执行时机 异步,不阻塞绘制 同步,阻塞绘制
适用场景 大多数副作用 DOM 测量和同步更新
服务端渲染 正常工作 会触发警告
性能影响 较小 可能较大(如果滥用)

何时使用 useLayoutEffect

  • 需要同步读取或修改 DOM 布局时
  • 需要避免视觉上的"闪烁"或布局跳动时
  • 在动画初始化或复杂交互场景中

总结

现在我相信你已经能够入门useLayoutEffect的基本概念、使用方法与应用场景了,如果想要了解更多的关于useLayoutEffect的详细知识,推荐可以去看React的官方文档

相关推荐
大佐不会说日语~14 分钟前
JVM垃圾回收机制面试笔记
jvm·笔记·面试
三月的一天15 分钟前
在 React Three Fiber 中实现 3D 模型点击扩散波效果
前端·react.js·前端框架
爱敲代码的小冰15 分钟前
npm 切换 node 版本 和npm的源
前端·npm·node.js
DoraBigHead20 分钟前
🧠【彻底读懂 reduce】acc 是谁?我是谁?我们要干嘛?
前端·javascript·面试
future141232 分钟前
项目开发日记
前端·学习·c#·游戏开发
汪子熙40 分钟前
CSS 中 td:last-child a 选择器详解
前端·javascript
q567315231 小时前
Koa+Puppeteer爬虫教程页面设计
javascript·css·爬虫
北北~Simple1 小时前
第一次搭建数据库
服务器·前端·javascript·数据库
GanGuaGua1 小时前
Vue3常用指令
前端·javascript·vue.js
欧阳天风1 小时前
录音实时上传
前端·javascript