说说 useEffect 和 useLayoutEffect 的区别?
问题解析
面试官考察点:
- 是否了解 useEffect 和 useLayoutEffect 的执行时机差异
- 是否理解浏览器渲染流程
- 能否根据场景选择合适的 Hook
- 对 React 渲染机制的理解
核心概念
两个 Hook 的定义
- useEffect:在浏览器绘制完成后异步执行
- useLayoutEffect:在浏览器绘制之前同步执行 n
浏览器渲染流程
1. React 渲染(生成 Virtual DOM)
2. React 提交(Commit Phase)
- 执行 useLayoutEffect
3. 浏览器绘制(Paint)
4. 执行 useEffect
详细解答
1. 基本用法对比
import React, { useEffect, useLayoutEffect, useRef } from 'react';
function Example() {
const divRef = useRef();
// useEffect - 异步执行,在绘制之后
useEffect(() => {
console.log('useEffect');
console.log('DOM width:', divRef.current.offsetWidth);
});
// useLayoutEffect - 同步执行,在绘制之前
useLayoutEffect(() => {
console.log('useLayoutEffect');
console.log('DOM width:', divRef.current.offsetWidth);
});
return <div ref={divRef}>Hello</div>;
}
// 输出顺序:
// 1. useLayoutEffect
// 2. useEffect
2. 执行时机差异
function TimingExample() {
const [width, setWidth] = useState(0);
const boxRef = useRef();
useLayoutEffect(() => {
// 在浏览器绘制前执行
// 可以获取到 DOM 的最新布局信息
console.log('useLayoutEffect - 同步执行');
const newWidth = boxRef.current.getBoundingClientRect().width;
// 同步修改状态,用户不会看到中间状态
if (newWidth > 500) {
setWidth(500); // 立即调整,避免闪烁
}
});
useEffect(() => {
// 在浏览器绘制后执行
console.log('useEffect - 异步执行');
// 可能看到布局闪烁
});
return <div ref={boxRef} style={{ width: width || '100%' }}>Content</div>;
}
3. 实际应用场景
useEffect 的典型场景
// 1. 数据获取
function UserList() {
const [users, setUsers] = useState([]);
useEffect(() => {
fetch('/api/users')
.then(res => res.json())
.then(data => setUsers(data));
}, []);
return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>;
}
// 2. 订阅/取消订阅
function ChatRoom({ roomId }) {
useEffect(() => {
const connection = createConnection(roomId);
connection.connect();
return () => {
connection.disconnect();
};
}, [roomId]);
return <div>Chat Room {roomId}</div>;
}
// 3. 手动修改 DOM(非布局相关)
function AutoFocusInput() {
const inputRef = useRef();
useEffect(() => {
// 聚焦不需要在绘制前执行
inputRef.current.focus();
}, []);
return <input ref={inputRef} />;
}
useLayoutEffect 的典型场景
// 1. 防止布局闪烁
function Tooltip({ children, content }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const tooltipRef = useRef();
useLayoutEffect(() => {
const rect = tooltipRef.current.getBoundingClientRect();
const viewportWidth = window.innerWidth;
// 如果 tooltip 超出视口右侧,调整到左侧显示
if (rect.right > viewportWidth) {
setPosition({ x: rect.left - rect.width, y: rect.top });
}
}, []);
return (
<div ref={tooltipRef} style={{ left: position.x, top: position.y }}>
{content}
</div>
);
}
// 2. 测量 DOM 并同步修改
function MeasureExample() {
const [height, setHeight] = useState(0);
const contentRef = useRef();
useLayoutEffect(() => {
// 测量内容高度
const measuredHeight = contentRef.current.scrollHeight;
// 同步设置高度,避免动画过程中的闪烁
setHeight(measuredHeight);
});
return (
<div style={{ height, transition: 'height 0.3s' }}>
<div ref={contentRef}>{/* 动态内容 */}</div>
</div>
);
}
// 3. 从 DOM 读取样式并同步修改
function ColorPicker() {
const [color, setColor] = useState('#000');
const canvasRef = useRef();
useLayoutEffect(() => {
// 从 canvas 获取像素颜色
const ctx = canvasRef.current.getContext('2d');
const pixel = ctx.getImageData(0, 0, 1, 1).data;
// 同步更新颜色
setColor(`rgb(${pixel[0]}, ${pixel[1]}, ${pixel[2]})`);
}, []);
return <canvas ref={canvasRef} />;
}
深入理解
渲染流程详解
function DetailedExample() {
console.log('1. Render Phase - 计算组件输出');
useLayoutEffect(() => {
console.log('3. Layout Effect - 在绘制前同步执行');
// 此时可以读取 DOM 布局信息
// 同步修改状态会阻塞绘制
});
useEffect(() => {
console.log('4. Passive Effect - 在绘制后异步执行');
// 不会阻塞绘制
});
return (
<div ref={() => console.log('2. Commit Phase - DOM 更新')}>
Content
</div>
);
}
性能影响
// 阻塞渲染的示例(慎用)
function BlockingExample() {
useLayoutEffect(() => {
// 耗时操作会阻塞浏览器绘制
const start = performance.now();
while (performance.now() - start < 100) {
// 阻塞 100ms
}
});
return <div>Content</div>;
}
// 正确的做法:耗时操作放在 useEffect
function NonBlockingExample() {
useEffect(() => {
// 不会阻塞绘制
const start = performance.now();
while (performance.now() - start < 100) {
// 阻塞 100ms
}
});
return <div>Content</div>;
}
SSR 注意事项
import React, { useEffect, useLayoutEffect } from 'react';
// 在服务端渲染时,useLayoutEffect 会发出警告
// 解决方案:使用 useEffect 替代,或条件判断
const useIsomorphicLayoutEffect = typeof window !== 'undefined'
? useLayoutEffect
: useEffect;
function SSRComponent() {
// 安全地在 SSR 中使用
useIsomorphicLayoutEffect(() => {
// 只在客户端执行
document.title = 'Updated';
}, []);
return <div>Content</div>;
}
最佳实践
1. 默认使用 useEffect
// 大多数情况下,useEffect 足够使用
function Component() {
const [data, setData] = useState(null);
useEffect(() => {
fetchData().then(setData);
}, []);
return <div>{data}</div>;
}
2. 仅在必要时使用 useLayoutEffect
// 当出现布局闪烁问题时,才考虑使用 useLayoutEffect
function Modal({ isOpen }) {
const [position, setPosition] = useState({ top: 0, left: 0 });
const modalRef = useRef();
useLayoutEffect(() => {
if (isOpen) {
const rect = modalRef.current.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// 确保 modal 在视口内
if (rect.bottom > viewportHeight) {
setPosition(prev => ({
...prev,
top: viewportHeight - rect.height - 20
}));
}
}
}, [isOpen]);
return (
<div ref={modalRef} style={{ position: 'absolute', ...position }}>
Modal Content
</div>
);
}
3. 避免在 useLayoutEffect 中执行耗时操作
// 错误:阻塞渲染
useLayoutEffect(() => {
heavyComputation(); // 耗时计算
}, []);
// 正确:异步执行耗时操作
useEffect(() => {
heavyComputation();
}, []);
对比总结
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 绘制完成后异步执行 | 绘制前同步执行 |
| 阻塞渲染 | 否 | 是 |
| 适用场景 | 数据获取、订阅、事件处理 | DOM 测量、防止闪烁 |
| 性能影响 | 小 | 大(可能阻塞) |
| SSR 支持 | 完整 | 需要特殊处理 |
| 使用频率 | 高(默认选择) | 低(特殊情况) |
面试要点
- 执行时机差异:useEffect 异步(绘制后),useLayoutEffect 同步(绘制前)
- 性能影响:useLayoutEffect 会阻塞浏览器绘制
- 适用场景:useEffect 用于大多数副作用,useLayoutEffect 用于布局相关
- SSR 注意:服务端渲染时 useLayoutEffect 会有警告
常见追问:
- 什么时候应该使用 useLayoutEffect 而不是 useEffect?
- useLayoutEffect 会阻塞渲染,为什么还需要它?
- 在服务端渲染时如何处理 useLayoutEffect?
- 如果在 useLayoutEffect 中 setState 会发生什么?