新手刚学 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:副作用的扫地僧
现在轮到我们讲 useEffect
和 useLayoutEffect
里的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 都会重新执行副作用逻辑,但它会:
- 先移除之前绑定的事件
- 然后再绑定新的
如果不清理,会怎样?
比如你写了个定时器,却没清理:
jsx
useEffect(() => {
const timer = setInterval(() => {
console.log('⏰ 计时中');
}, 1000);
}, [count]);
那么每次 count
变化,都会重新创建一个定时器。
- 你本来只想计时一次
- 结果页面上同时跑了 N 个定时器
- 控制台疯狂输出,用户电脑风扇起飞💻🌪️
总结:依赖项变化 → 触发清理函数!
场景 | 是否执行清理函数? | 说明 |
---|---|---|
第一次渲染 | ❌ 不执行 | 因为没有旧的 effect |
依赖项变化 | ✅ 执行 | 清理旧的,再执行新的 |
依赖项没变 | ❌ 不执行 | effect 根本不重新运行 |
组件卸载 | ✅ 执行 | 清理最后一次的副作用 |
接下来我来介绍一下useLayoutEffect:
那到底啥时候该用 useLayoutEffect?
默认都用 useEffect
就行,性能好,代码清晰。
只有这几种情况才推荐用 useLayoutEffect
:
- 你需要读取 DOM 的尺寸 (比如
getBoundingClientRect()
) - 你要设置某个样式值,否则页面会先跳一下(比如动画初始状态)
- 你要阻止某些东西在绘制前出现(比如 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 小白头秃症!