要搞懂 useInsertionEffect、useLayoutEffect、useEffect 的区别,核心是抓住 执行时机 和 适用场景 ------ 这三个 Hook 都是 React 处理"副作用"的工具,但执行阶段、阻塞特性、适用场景完全不同,选对了能避免渲染闪烁、样式失效等问题。
先看一张核心对比表,快速建立认知:
| 特性 | useEffect | useLayoutEffect | useInsertionEffect |
|---|---|---|---|
| 执行时机 | 组件渲染完成(屏幕绘制后) | 组件渲染完成(屏幕绘制前) | DOM 生成后、布局计算前(React 18+ 新增) |
| 是否阻塞渲染 | 不阻塞(异步) | 阻塞(同步) | 阻塞(同步,更早) |
| 能否操作 DOM | 能(但可能导致闪烁) | 能(无闪烁) | 能(仅用于插入样式) |
| 触发时机排序 | 最晚 | 中间 | 最早 |
| 核心用途 | 数据请求、事件监听、异步操作 | DOM 测量、同步修改 DOM、避免闪烁 | CSS-in-JS 插入样式、避免样式计算重复 |
一、逐个拆解:执行时机 + 核心用法
1. useEffect(最常用)
-
执行时机 :组件首次渲染/依赖更新后 → 浏览器完成屏幕绘制 → 执行
useEffect回调(异步,不阻塞页面显示)。 -
核心特点:和浏览器"绘制"解耦,是 React 推荐的默认副作用 Hook。
-
适用场景 :
- 数据请求(接口调用)、异步操作;
- 订阅/取消订阅(如事件监听、WebSocket);
- 不依赖 DOM 布局的操作(如修改状态、记录日志)。
-
示例 :
jsimport { useEffect, useState } from 'react'; function UserList() { const [users, setUsers] = useState([]); // 组件渲染完成后请求数据(不阻塞页面) useEffect(() => { const fetchUsers = async () => { const res = await fetch('/api/users'); setUsers(await res.json()); }; fetchUsers(); // 清理副作用(组件卸载时取消监听) return () => { // 比如取消定时器、移除事件监听 }; }, []); // 空依赖:仅首次渲染执行 return <div>{users.map(u => <p key={u.id}>{u.name}</p>)}</div>; }
2. useLayoutEffect(同步操作 DOM)
-
执行时机 :组件首次渲染/依赖更新后 → React 完成 DOM 变更 → 浏览器未绘制屏幕 → 同步执行
useLayoutEffect回调 → 执行完成后浏览器才绘制屏幕(阻塞绘制)。 -
核心特点 :比
useEffect早执行,同步阻塞渲染,能避免 DOM 操作导致的"视觉闪烁"。 -
适用场景 :
- DOM 测量(如获取元素宽高、位置)并同步修改 DOM;
- 避免闪烁的样式修改(比如根据元素位置调整弹窗位置);
- 依赖 DOM 布局的同步操作。
-
示例(解决测量 DOM 导致的闪烁) :
jsimport { useLayoutEffect, useRef, useState } from 'react'; function Popup() { const [width, setWidth] = useState(0); const ref = useRef(null); // useLayoutEffect:绘制前测量,无闪烁 useLayoutEffect(() => { // 同步获取 DOM 宽度(此时 DOM 已生成,未绘制) const w = ref.current.offsetWidth; setWidth(w); // 修改状态后,React 会在绘制前重新计算布局 }, []); // 若用 useEffect:绘制后测量,会先显示 0,再更新为实际宽度(闪烁) // useEffect(() => { ... }, []); return <div ref={ref}>弹窗宽度:{width}px</div>; }
3. useInsertionEffect(React 18+ 新增,专用于样式插入)
-
执行时机 :React 生成 DOM 节点后 →
useLayoutEffect执行前 → 浏览器计算布局/绘制前(是三个 Hook 中最早执行的)。 -
核心特点 :专为 CSS-in-JS 库设计,解决"样式插入时机晚于布局计算"导致的性能问题,只能用于插入样式,不能修改状态(修改状态会触发额外渲染)。
-
适用场景 :
- CSS-in-JS 库插入动态样式(如 styled-components、emotion);
- 需在布局计算前插入样式,避免浏览器重复计算布局。
-
示例(CSS-in-JS 插入样式) :
jsimport { useInsertionEffect } from 'react'; // 自定义 CSS-in-JS 样式插入 Hook function useInjectStyle(style) { useInsertionEffect(() => { // 布局计算前插入样式,避免重复计算 const styleTag = document.createElement('style'); styleTag.innerHTML = style; document.head.appendChild(styleTag); return () => { document.head.removeChild(styleTag); }; }, [style]); } function StyledButton() { // 插入动态样式 useInjectStyle(` .btn { background: blue; color: white; padding: 8px 16px; } `); return <button className="btn">自定义样式按钮</button>; }
二、关键执行顺序(实战必懂)
以组件首次渲染为例,完整执行流程:
1. React 渲染组件 → 生成 DOM 节点(未布局、未绘制)
2. 执行 useInsertionEffect 回调(仅插入样式,不修改状态)
3. 浏览器计算 DOM 布局(宽高、位置)
4. 执行 useLayoutEffect 回调(同步修改 DOM/状态,可能触发重新布局)
5. 浏览器绘制屏幕(页面显示)
6. 执行 useEffect 回调(异步,不阻塞)
三、避坑指南(新手常错)
1. 不要滥用 useLayoutEffect
useLayoutEffect 阻塞绘制,若回调内逻辑复杂(如大量计算),会导致页面加载变慢。能用 useEffect 解决的,就不用 useLayoutEffect。
2. useInsertionEffect 不能修改状态
useInsertionEffect 执行时,React 还未完成"布局提交",修改状态会触发额外的布局计算,导致性能问题 ------ 它的唯一用途是插入样式。
3. 依赖项的坑
三个 Hook 的依赖项规则一致:
- 空数组
[]:仅首次渲染执行; - 依赖变量:变量变化时执行;
- 无依赖:每次渲染都执行。
4. 服务端渲染(SSR)注意
-
useInsertionEffect是唯一能在 SSR 中安全插入样式的 Hook(useLayoutEffect在 SSR 中会报警告); -
若用
useLayoutEffect做 SSR,需加判断:jsconst isClient = typeof window !== 'undefined'; useLayoutEffect(() => { if (isClient) { // 仅客户端执行 DOM 操作 } }, []);
总结
- 选 useEffect:绝大多数场景(数据请求、异步操作、事件监听),默认首选;
- 选 useLayoutEffect:需要同步操作 DOM、测量布局、避免视觉闪烁时;
- 选 useInsertionEffect:React 18+ 中做 CSS-in-JS 样式插入,或需在布局计算前插入样式时。
核心记忆点:
- 执行顺序:
useInsertionEffect→useLayoutEffect→useEffect; - 阻塞性:前两个阻塞渲染,
useEffect不阻塞; - 专用性:
useInsertionEffect仅用于样式插入,不要挪作他用。
如果有具体场景(比如"修改样式导致闪烁""CSS-in-JS 样式不生效"),可以贴出代码,我帮你选对应的 Hook 并优化。