🍱 面试现场:外卖引发的血案
面试官:(突然亮出手机)你看这个外卖App,下单后页面要刷新数据。用useEffect
还是useLayoutEffect
?
我:(战术微笑)这得看您是想要饿肚子等大餐(useEffect)还是厨房现场监工(useLayoutEffect)!
面试官:(怒点送达按钮)说人话!
我:如果数据刷新不怕页面闪动就用useEffect
,要避免UI抖动就用useLayoutEffect
!
🕰️ 核心差异:React的时空法则
1. 执行时机对比
// 浏览器渲染流水线
1. 组件渲染 → 2. DOM更新 → 3. 浏览器绘制 → 4. useEffect执行
↗
useLayoutEffect插队 → 2.5. useLayoutEffect执行
2. 可视化比喻
useEffect | useLayoutEffect | |
---|---|---|
性格 | 慢性子外卖小哥 | 急性子急诊医生 |
工作方式 | 等页面画完再干活 | 必须立刻马上处理 |
适用场景 | 数据获取/日志记录 | DOM尺寸测量/紧急调整 |
🔍 一、执行流程深度解剖
实验1:闪烁的副作用
function BlinkComponent() {
const [width, setWidth] = useState(0);
const divRef = useRef();
useLayoutEffect(() => {
// 在绘制前测量
setWidth(divRef.current.offsetWidth);
}, []);
useEffect(() => {
// 这里测量会导致二次渲染抖动
// setWidth(divRef.current.offsetWidth);
}, []);
return <div ref={divRef}>宽度:{width}px</div>;
}
现象:
useLayoutEffect
:宽度直接正确显示useEffect
:先显示0,再闪动到实际值
实验2:事件监听竞速赛
useEffect(() => {
const handleClick = () => console.log('Effect监听');
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
useLayoutEffect(() => {
const handleClick = () => console.log('Layout监听');
document.addEventListener('click', handleClick);
return () => document.removeEventListener('click', handleClick);
}, []);
打印顺序:
- Layout监听 ← 先绑定
- Effect监听 ← 后绑定
执行顺序:LayoutEffect清理 → LayoutEffect执行 → Effect清理 → Effect执行
🕵️ 二、获取前朝遗老:上一次的state
方案1:useRef时空胶囊
function Component() {
const [count, setCount] = useState(0);
const prevCountRef = useRef();
useEffect(() => {
prevCountRef.current = count; // 本次渲染结束后保存
});
return (
<div>
当前:{count},之前:{prevCountRef.current}
</div>
);
}
方案2:自定义Hook时间机器
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value; // 在effect中更新
});
return ref.current;
}
// 使用
const prevCount = usePrevious(count);
方案3:函数式更新考古
const [user, setUser] = useState({ id: 1, name: '老王' });
// 修改时捕获旧值
const updateUser = () => {
setUser(prevUser => {
console.log('旧值:', prevUser);
return { ...prevUser, name: '隔壁老王' };
});
};
💥 三、高频灵魂拷问
Q1:两者会阻塞渲染吗?
// useLayoutEffect会!
// 伪代码实现
function commitWork() {
commitDOMUpdates(); // 更新DOM
flushLayoutEffects(); // 同步执行LayoutEffect
requestPaint(); // 通知浏览器绘制
scheduleEffects(); // 异步调度Effect
}
结论:
useLayoutEffect
:同步执行,可能阻塞渲染useEffect
:异步执行,不阻塞
Q2:服务端渲染(SSR)能用吗?
// 服务端会警告!
function ServerComponent() {
useLayoutEffect(() => {
// 服务端没有DOM,这里会报错
console.log(document.body);
}, []);
}
逃生方案:
const useIsomorphicLayoutEffect =
typeof window !== 'undefined' ? useLayoutEffect : useEffect;
Q3:如何避免无限循环?
// 危险操作
useEffect(() => {
setCount(count + 1); // 每次执行都改依赖项
}, [count]);
// 正确做法
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 函数式更新不依赖count
}, 1000);
return () => clearInterval(timer);
}, []); // 空依赖
🚀 四、性能优化黑魔法
1. 副作用防抖术
useEffect(() => {
const handler = setTimeout(() => {
// 延迟执行避免高频更新
fetchData(query);
}, 500);
return () => clearTimeout(handler);
}, [query]); // query变化时重置定时器
2. 依赖数组缩骨功
const [user, setUser] = useState({ id: 1, name: '老王' });
useEffect(() => {
// 只关心id变化
}, [user.id]); // 而不是整个user对象
3. 清理函数分身术
useLayoutEffect(() => {
const element = ref.current;
const observer = new ResizeObserver(handleResize);
observer.observe(element);
return () => {
observer.unobserve(element); // 精准清理
observer.disconnect();
};
}, []);
🎙️ 面试官の终极大招
面试官:(突然掏出VR眼镜)如果在这个虚拟列表组件里,同时用useEffect
和useLayoutEffect
处理滚动位置,会发生什么?
我:(戴上眼镜)这得看VR设备的渲染管线!如果是WebGL渲染:
useLayoutEffect
:在DOM更新后立即修正位置,避免视觉错位useEffect
:可能等到下一帧才调整,用户会看到闪烁
不过...(突然摘掉眼镜)您这眼镜根本没开机啊!
后记:后来在真实项目中优化滚动列表时,发现useLayoutEffect
配合requestAnimationFrame
才是王道。就像外卖准时达,技术选型也要卡准时机!(完)