useEffect vs useLayoutEffect 完全指南
🎯 核心区别:执行时机
useEffect(异步副作用)
markdown
React 渲染 DOM
↓
浏览器绘制屏幕
↓
执行 useEffect ← 用户已经看到 DOM
↓
可能导致屏幕闪烁
useLayoutEffect(同步副作用)
markdown
React 渲染 DOM
↓
执行 useLayoutEffect ← 立即同步执行
↓
浏览器绘制屏幕(已是最终效果)
↓
用户看到最终结果(无闪烁)
📊 详细对比表
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | DOM 更新后,浏览器绘制前 | DOM 更新后,立即同步执行 |
| 阻塞渲染 | ❌ 不阻塞 | ✅ 会阻塞浏览器绘制 |
| 性能 | ⭐⭐⭐⭐⭐ 更好 | ⭐⭐⭐ 可能卡顿 |
| 使用频率 | 99% 的情况用这个 | 特定场景用 |
| 服务端渲染 | ✅ 支持 | ❌ 会报警告 |
| 清理函数执行 | DOM 更新前执行 | DOM 更新前执行 |
🔴 useEffect 执行时间线
javascript
function Component() {
useEffect(() => {
console.log('1. useEffect 执行');
return () => console.log('2. useEffect 清理函数');
}, []);
console.log('0. 组件渲染');
return <div>Hello</div>;
}
// 执行顺序:
// 0. 组件渲染
// 1. useEffect 执行
// 2. useEffect 清理函数(卸载或依赖变化时)
🟠 useLayoutEffect 执行时间线
javascript
function Component() {
useLayoutEffect(() => {
console.log('1. useLayoutEffect 执行(同步)');
return () => console.log('2. useLayoutEffect 清理函数');
}, []);
console.log('0. 组件渲染');
return <div>Hello</div>;
}
// 执行顺序:
// 0. 组件渲染
// 1. useLayoutEffect 执行(同步,阻塞浏览器绘制)
// 浏览器绘制屏幕
// 2. useLayoutEffect 清理函数
💡 常用场景 & 代码示例
场景1️⃣:数据请求(useEffect)✅ 推荐
javascript
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(false);
useEffect(() => {
setLoading(true);
// 异步获取数据
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
})
.catch(err => {
console.error('加载失败:', err);
setLoading(false);
});
}, [userId]); // userId 变化时重新获取
if (loading) return <div>加载中...</div>;
return <div>{user?.name}</div>;
}
为什么用 useEffect?
- 数据获取是异步操作,不需要同步阻塞
- 用户看到加载状态很正常
场景2️⃣:事件监听(useEffect)✅ 推荐
javascript
function WindowResize() {
const [width, setWidth] = useState(window.innerWidth);
useEffect(() => {
function handleResize() {
setWidth(window.innerWidth);
}
// 添加监听
window.addEventListener('resize', handleResize);
// 清理:移除监听
return () => {
window.removeEventListener('resize', handleResize);
};
}, []); // 只在挂载时运行
return <div>窗口宽度: {width}px</div>;
}
为什么用 useEffect?
- 事件监听是异步的,不需要同步执行
- 必须清理事件监听器,防止内存泄漏
场景3️⃣:DOM 测量(useLayoutEffect)✅ 正确
javascript
function MeasureElement() {
const ref = useRef(null);
const [height, setHeight] = useState(0);
useLayoutEffect(() => {
// 必须用 useLayoutEffect,否则会闪烁
// 1. 测量 DOM 尺寸
const rect = ref.current.getBoundingClientRect();
// 2. 基于尺寸计算布局
const newHeight = rect.width > 500 ? 300 : 150;
// 3. 立即更新(在浏览器绘制前)
setHeight(newHeight);
}, []); // 只在挂载时运行
return (
<div ref={ref} style={{ height: `${height}px` }}>
响应式高度
</div>
);
}
为什么用 useLayoutEffect?
- 需要在浏览器绘制前完成 DOM 测量和布局计算
- 如果用 useEffect,用户会看到高度从 0 闪到 300(闪烁!)
场景4️⃣:表单焦点设置(useLayoutEffect)✅ 正确
javascript
function AutoFocusForm() {
const inputRef = useRef(null);
useLayoutEffect(() => {
// 必须用 useLayoutEffect,否则表单打开时看不到光标在哪
inputRef.current?.focus();
}, []);
return (
<form>
<input ref={inputRef} placeholder="自动聚焦" />
<button type="submit">提交</button>
</form>
);
}
为什么用 useLayoutEffect?
- 如果用 useEffect,用户会看到输入框出现,然后光标才聚焦(视觉延迟)
- useLayoutEffect 保证光标在浏览器绘制前已经聚焦
场景5️⃣:动画初始化(useLayoutEffect)✅ 正确
javascript
function AnimatedBox() {
const boxRef = useRef(null);
useLayoutEffect(() => {
// 设置初始状态(隐藏)
gsap.set(boxRef.current, { opacity: 0, x: -50 });
// 然后执行动画
gsap.to(boxRef.current, {
opacity: 1,
x: 0,
duration: 0.5
});
}, []);
return <div ref={boxRef}>动画盒子</div>;
}
为什么用 useLayoutEffect?
- 避免用户看到初始未变换的状态,然后才动画
- useLayoutEffect 保证在浏览器绘制前完成初始设置
场景6️⃣:主题切换(useLayoutEffect)⚠️ 特殊
javascript
function ThemeProvider({ theme }) {
useLayoutEffect(() => {
// 立即应用主题,避免闪烁
document.documentElement.style.colorScheme = theme;
document.body.className = `theme-${theme}`;
}, [theme]);
return <div>内容</div>;
}
为什么用 useLayoutEffect?
- 防止主题切换时的闪烁(深色→浅色 的白屏闪烁)
📋 依赖数组详解
javascript
// 1️⃣ 每次渲染都执行(没有依赖数组)
useEffect(() => {
console.log('每次都运行');
});
// 2️⃣ 只在挂载时执行(空依赖数组)
useEffect(() => {
console.log('只在挂载时执行');
}, []);
// 3️⃣ 依赖值变化时执行
useEffect(() => {
console.log('userId 变化时执行');
}, [userId]);
// 4️⃣ 多个依赖
useEffect(() => {
console.log('userId 或 pageNum 变化时执行');
}, [userId, pageNum]);
// 5️⃣ 依赖是对象时的坑
useEffect(() => {
// ❌ 每次都会执行,因为 {} 每次都是新的
console.log('可能无限循环');
}, [{}]);
// ✅ 正确做法:使用 useMemo
const config = useMemo(() => ({}), []);
useEffect(() => {
console.log('config 稳定,不会无限循环');
}, [config]);
⚠️ 常见陷阱
陷阱1️⃣:useLayoutEffect 导致卡顿
javascript
// ❌ 不好:useLayoutEffect 中做复杂计算会阻塞绘制
useLayoutEffect(() => {
// 这会冻结浏览器!
for (let i = 0; i < 1000000; i++) {
complexCalculation();
}
}, []);
// ✅ 好:复杂计算放到 useEffect
useEffect(() => {
for (let i = 0; i < 1000000; i++) {
complexCalculation();
}
}, []);
陷阱2️⃣:useLayoutEffect 在服务端渲染中报错
javascript
// ❌ 错:SSR 时会报警告
useLayoutEffect(() => {
// SSR 中不能执行
}, []);
// ✅ 好:检测环境
useEffect(() => {
if (typeof window !== 'undefined') {
// 客户端代码
}
}, []);
// 或者条件性使用
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
useIsomorphicLayoutEffect(() => {
// 自动适配 SSR
}, []);
陷阱3️⃣:忘记清理副作用
javascript
// ❌ 坏:内存泄漏
useEffect(() => {
const timer = setInterval(() => {
console.log('每秒执行');
}, 1000);
// 没有清理!
}, []);
// ✅ 好:返回清理函数
useEffect(() => {
const timer = setInterval(() => {
console.log('每秒执行');
}, 1000);
return () => clearInterval(timer); // 清理定时器
}, []);
陷阱4️⃣:无限循环
javascript
// ❌ 坏:无限循环
useEffect(() => {
setCount(count + 1); // 每次都修改 count
}, [count]); // 导致重新渲染,又触发 effect
// ✅ 好:不把更新的值放在依赖中
useEffect(() => {
setCount(c => c + 1);
}, []); // 空依赖,只执行一次
🚀 选择流程图
markdown
需要执行副作用?
↓
是异步操作(数据获取、定时器)?
├─ 是 → useEffect ✅
└─ 否 ↓
需要在浏览器绘制前同步执行?
├─ 是 → useLayoutEffect ✅
│ 例:DOM 测量、焦点、动画、主题
└─ 否 → useEffect ✅(默认选择)
📊 性能对比
javascript
// 测试性能影响
function PerformanceTest() {
const [count, setCount] = useState(0);
// useEffect 不会阻塞渲染
useEffect(() => {
// 即使这里做复杂计算,页面也能快速响应
for (let i = 0; i < 10000000; i++) {}
}, [count]);
// useLayoutEffect 会阻塞渲染
useLayoutEffect(() => {
// 这个计算会让点击按钮时出现明显延迟
for (let i = 0; i < 10000000; i++) {}
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>
点击:{count}
</button>
);
}
// 结论:
// useEffect 按钮响应迅速
// useLayoutEffect 点击时会卡顿
✅ 快速参考
99% 情况用 useEffect
javascript
// 数据获取
useEffect(() => { fetchData(); }, []);
// 事件监听
useEffect(() => {
window.addEventListener('resize', handler);
return () => window.removeEventListener('resize', handler);
}, []);
// 副作用通用场景
useEffect(() => { doSomething(); }, [dep]);
特殊场景用 useLayoutEffect
javascript
// DOM 测量
useLayoutEffect(() => {
const rect = ref.current.getBoundingClientRect();
setHeight(rect.height);
}, []);
// 焦点设置
useLayoutEffect(() => {
inputRef.current?.focus();
}, []);
// 动画初始化
useLayoutEffect(() => {
gsap.set(ref.current, initialState);
startAnimation();
}, []);
🎓 总结
| 维度 | useEffect | useLayoutEffect |
|---|---|---|
| 何时用 | 99% 的情况 | 需要同步 DOM 操作 |
| 执行时机 | 浏览器绘制后 | 浏览器绘制前 |
| 是否阻塞 | 不阻塞 | 会阻塞 |
| 性能 | 优秀 | 可能卡顿 |
| 例子 | 数据请求、事件监听 | DOM 测量、焦点、动画 |
记住:优先用 useEffect,只有特殊场景才用 useLayoutEffect! 🎯