React 中的 useEffect 和 useLayoutEffect:你到底在什么时候动我 DOM?

新手刚学 React,听说 useEffect 是"副作用 Hook",以为能当万能胶水一顿乱抹;用着用着,又跳出个 useLayoutEffect,说它更"同步"、"更底层",甚至还能阻塞浏览器渲染?!

到底怎么回事?今天我们就来一篇讲明白的文章,保证你看完不再抓头皮!


为什么有 useEffect?

我们先从最基础的说起。

你写 React 组件时,里面不能直接写定时器、发请求、加事件监听这些副作用操作,因为它们不是"纯粹的 UI 渲染"。

这时候,React 就给你一把工具锤

scss 复制代码
useEffect(() => {
  // 这里面你想干啥都可以(别太离谱)
}, []);

这玩意的官方名叫:"副作用钩子"。


useEffect 的执行时机:页面都画完了,我才出场!

重点来了:useEffect执行时机 非常佛系------它不参与页面构建过程,等所有 DOM 更新好了,浏览器也把页面画完了,才悠悠地执行。

流程长这样:

复制代码
函数组件执行 → 虚拟 DOM 构建 → 真实 DOM 更新 → 页面绘制完成 → ✅ useEffect 执行!

举个栗子 :

scss 复制代码
useEffect(() => {
  console.log('页面都画好了,我才动手!');
}, []);

那 useLayoutEffect 是谁?又来抢戏了?

听名字就知道,useLayoutEffect 是 useEffect 的卷王表亲。

它的执行时机更早:就在真实 DOM 已经更新完,但是浏览器还没来得及"上色"之前。

简单说:

复制代码
函数组件执行 → DOM 更新完毕 → ✅ useLayoutEffect 执行! → 页面绘制

你可以理解成:useLayoutEffect 在最后一刻插队说:"嘿,我还有个事没干,等我一下!"


两者对比图让你一眼明白

特性 useEffect useLayoutEffect
是否异步 ✅ 是 ❌ 否(同步)
DOM 是否已更新 ✅ 是 ✅ 是
页面是否已绘制 ✅ 是 ❌ 否
是否阻塞绘制 ❌ 否 ✅ 是
常见用途 请求、事件、订阅 动画初始设置、DOM 测量

清理函数 return:副作用的扫地僧

现在轮到我们讲 useEffectuseLayoutEffect 里的return 清理函数了,它干嘛的?

当你设置了一个副作用,比如监听滚动、设置定时器,那你必须在不需要的时候手动"清理现场"。

不然副作用就会变成副作用 (内存泄漏、报错等)。

下面是一个经典的案例

示例:定时器清理

jsx 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('滴...滴...');
  }, 1000);

  return () => {
    clearInterval(timer);
    console.log(' 定时器清理完成');
  };
}, []);

每次依赖变化前 / 组件卸载时,都会执行 return 里面的逻辑。 下面我来详细解释一下这部分内容

一句话总结先来:

每次依赖变化前,或者组件卸载时,React 会执行上一次 useEffect 的清理函数。

听起来挺抽象?别急,我们接下来按节奏拆解 + 举例,一口吃下!


我们先从 useEffect 的依赖项说起

js 复制代码
useEffect(() => {
  console.log('副作用执行');
  
  return () => {
    console.log('🧹 清理函数执行');
  };
}, [count]);

这个 count 就是依赖项,只有当 count 发生变化时,这个 effect 才会重新执行。

但------在重新执行 之前 ,React 会先把上一次的"副作用"先给清掉,也就是执行你 return 里的清理函数。


执行顺序图解:

我们假设组件多次触发 count 更新,流程如下:

rust 复制代码
第1次 count = 0:
  -> useEffect 执行副作用
    ✅ 打印 "副作用执行"

第2次 count = 1:
  -> 💥 先执行上一次的清理函数
    ✅ 打印 "🧹 清理函数执行"
  -> 然后执行新的副作用
    ✅ 打印 "副作用执行"

第3次 count = 2:
  -> 💥 清理上一次的副作用
    ✅ 打印 "🧹 清理函数执行"
  -> 再执行新的副作用
    ✅ 打印 "副作用执行"

组件卸载:
  -> 💥 最后执行一次清理函数
    ✅ 打印 "🧹 清理函数执行"

为什么要这样设计?(React 的设计哲学)

因为副作用往往会产生持久的影响,比如:

  • 开了一个定时器
  • 绑定了事件监听
  • 发了网络请求
  • 设置了某些全局状态

这些副作用如果不清理,很容易造成 内存泄漏逻辑错误多次绑定 等问题!

所以 React 非常贴心地说:

"你这副作用我看着是要更新了,我先帮你把上一个清掉啊!"


经典例子:监听窗口大小变化

jsx 复制代码
useEffect(() => {
  const handleResize = () => {
    console.log('📏 window resized');
  };

  window.addEventListener('resize', handleResize);

  return () => {
    console.log('🧹 清理 resize 监听');
    window.removeEventListener('resize', handleResize);
  };
}, [count]);

每次 count 改变,React 都会重新执行副作用逻辑,但它会:

  1. 先移除之前绑定的事件
  2. 然后再绑定新的

如果不清理,会怎样?

比如你写了个定时器,却没清理:

jsx 复制代码
useEffect(() => {
  const timer = setInterval(() => {
    console.log('⏰ 计时中');
  }, 1000);
}, [count]);

那么每次 count 变化,都会重新创建一个定时器。

  • 你本来只想计时一次
  • 结果页面上同时跑了 N 个定时器
  • 控制台疯狂输出,用户电脑风扇起飞💻🌪️

总结:依赖项变化 → 触发清理函数!

场景 是否执行清理函数? 说明
第一次渲染 ❌ 不执行 因为没有旧的 effect
依赖项变化 ✅ 执行 清理旧的,再执行新的
依赖项没变 ❌ 不执行 effect 根本不重新运行
组件卸载 ✅ 执行 清理最后一次的副作用

接下来我来介绍一下useLayoutEffect

那到底啥时候该用 useLayoutEffect?

默认都用 useEffect 就行,性能好,代码清晰。

只有这几种情况才推荐用 useLayoutEffect

  1. 你需要读取 DOM 的尺寸 (比如 getBoundingClientRect()
  2. 你要设置某个样式值,否则页面会先跳一下(比如动画初始状态)
  3. 你要阻止某些东西在绘制前出现(比如 loading 前别让旧数据出现)

一个真实 DOM 操作案例

ini 复制代码
const boxRef = useRef();

useLayoutEffect(() => {
  const width = boxRef.current.getBoundingClientRect().width;
  console.log('盒子宽度:', width);
}, []);

如果你用 useEffect,可能 DOM 已经画出来了,但你测量的是 旧尺寸 或者视觉上会抖动!


别滥用 useLayoutEffect(因为它是同步的!)!

虽然它能插队、能卡页面,但不是越早执行越香!

React 官方建议:

"如果你不确定是不是应该用 useLayoutEffect,那你大概率不应该用它。"

我来以一个表格总结一下为什么不能滥用

特性 useEffect useLayoutEffect
是否异步 ✅ 是 ❌ 否(同步)
DOM 是否已更新 ✅ 是 ✅ 是
页面是否已绘制 ✅ 是 ❌ 否
是否阻塞绘制 ❌ 否 ✅ 是
常见用途 请求、事件、订阅 动画初始设置、DOM 测量

总结:React 副作用三件套,得会用!

场景 推荐 Hook 是否需要清理函数?
网络请求、事件监听、日志等 useEffect ✅ 需要
测量 DOM、布局计算、动画初始状态 useLayoutEffect ✅ 需要
无副作用(纯 UI 渲染) ❌ 不需要 Hook ❌ 不需要

🔚 最后,送你一句话记忆口诀:

🌈 "副作用晚点做(useEffect),测量动画要插队(useLayoutEffect)。"

副作用要扫地(清理函数 return),不扫你就等着泄漏!


关注小阳不迷路,专治 React 小白头秃症!

相关推荐
Tomorrow'sThinker8 分钟前
了解 ReAct 框架:语言模型中推理与行动的协同
javascript·react.js·语言模型
可缺不可滥25 分钟前
Chrome 开发环境屏蔽 CORS 跨域限制
前端·chrome
神仙别闹31 分钟前
基于Java+MySQL实现(Web)文件共享管理系统(仿照百度文库)
java·前端·mysql
三月的一天36 分钟前
React Three Fiber 实现昼夜循环:从光照过渡到日月联动的技术拆解
前端·react.js·前端框架
前端 贾公子37 分钟前
Vue (Official) v3.0.2 新特性 为非类npm环境引入 globalTypesPath 选项
前端·vue.js·npm
前端程序猿-秦祥1 小时前
uniapp打开导航软件并定位到目标位置的实现
前端·uni-app·vue·导航
拾光拾趣录1 小时前
小程序双线程架构:为什么需要两个线程才能跳舞?
前端·微信小程序
天下无贼!1 小时前
【样式效果】Vue3实现仿制iOS按钮动态效果
前端·css·vue.js·ios
伍哥的传说1 小时前
React 英语单词补全游戏——一个寓教于乐的英语单词记忆游戏
react.js·游戏·c#·anime.js·英语单词大冒险·单词记忆·webspeechapi
捡芝麻丢西瓜1 小时前
iOS 异步任务 之 内存隔离
前端·ios