useEffect 和 useLayoutEffect 有什么区别?
好的,useEffect 和 useLayoutEffect 是 React Hooks 中两个用于处理副作用的 API,它们的主要区别在于执行时机。理解这个区别对于避免页面闪烁、优化性能至关重要。
下面通过一个表格和详细解释来说明它们的区别。
核心区别对比表
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器完成绘制(paint)之后,异步执行。 | DOM 更新完成后,浏览器绘制之前,同步执行。 |
| 阻塞渲染 | 否。不会阻塞浏览器绘制。 | 是。会阻塞浏览器绘制,直到其回调执行完毕。 |
| 使用场景 | 绝大多数副作用操作:数据获取、订阅、手动修改 DOM(不影响布局)、日志记录等。 | 需要读取 DOM 布局并同步触发重新渲染的场景,例如测量元素尺寸或位置后立即调整 UI。 |
| 类比于 Class 组件 | componentDidMount, componentDidUpdate, componentWillUnmount (但执行时机更晚) |
componentDidMount, componentDidUpdate (但与渲染阶段同步) |
详细解释与示例
1. useEffect - “事后处理”
- 工作原理:当函数组件进行渲染(Render)后,React 会将
useEffect的回调函数推入一个队列中,等待当前渲染周期结束、浏览器完成屏幕绘制后,再在下一个事件循环中按顺序执行。 - 优点:因为它不阻塞浏览器的绘制过程,所以不会导致页面卡顿或白屏,用户体验更流畅。
- 常见用例:
- API 数据获取
- 设置订阅或事件监听器
- 手动操作 DOM(如添加动画类),但这些操作不会影响页面的初始布局计算。
jsx
import { useState, useEffect } from 'react';
function MyComponent() {
const [count, setCount] = useState(0);
// ✅ Good: useEffect for data fetching or non-blocking side effects.
useEffect(() => {
console.log('useEffect: Count updated!', count);
document.title = `You clicked ${count} times`;
// Cleanup function (e.g., unsubscribe)
return () => {
console.log('Cleanup effect');
};
}, [count]);
return <button onClick={() => setCount(c => c + 1)}>Click me</button>;
}
2. useLayoutEffect - “事前干预”
- 工作原理:当函数组件进行渲染后,在浏览器真正将像素点画到屏幕上之前,React会同步地调用
useLayoutEffect的回调函数。它会阻塞后续的绘制流程。 - 缺点:如果回调函数中包含耗时操作,会导致明显的延迟感,用户可能会看到空白页或卡顿。
- 适用场景:当你需要根据最新的 DOM 布局信息来做一些必须立即生效且不能让用户看到的“闪动”的操作时。
- 测量 DOM:比如获取元素的宽度、高度、滚动位置等。
- 根据测量结果立即修改样式或状态:以避免用户看到中间的不一致状态。
jsx
import { useState, useRef, useLayoutEffect } from 'react';
function Tooltip() {
const ref = useRef();
// ⚠️ Use case for useLayoutEffect: Measuring layout and synchronously updating state.
// Prevents flicker by applying changes before the browser paints.
useLayoutEffect(() => {
const { width } = ref.current.getBoundingClientRect();
if (width > window.innerWidth) {
// If the element is too wide, we adjust its style immediately.
// This happens BEFORE the user sees the original oversized element.
ref.current.style.transform = 'scale(0.8)';
}
});
return <div ref={ref}>I might be a very long tooltip that overflows!</div>;
}
“闪动”问题的经典示例
假设我们有一个需求:点击按钮切换一个 div,并且希望它从顶部出现。
错误使用 useEffect(可能导致闪动):
- 组件因状态改变而重新渲染,
<div>的位置通过 CSS(position: absolute; top: ?px)决定。 - 由于状态变化,
<div>的初始位置可能是错误的(比如还在屏幕外)。 - 浏览器开始绘制这个“错误”的画面(用户可能瞥见一瞬间的跳动)。
- 然后,
useEffect中的回调才运行,计算出正确位置并更新状态/样式。 - 组件再次重新渲染并显示正确位置。(用户看到了两次画面)
- 组件因状态改变而重新渲染,
正确使用 useLayoutEffect(无闪动):
- 组件因状态改变而重新渲染,
<div>的初始位置同样是错误的。 - 在浏览器即将绘制前,同步地运行
useLayoutEffect。它读取 DOM(此时还是旧状态),计算出正确的新位置和样式,直接写入到 DOM(或通过更新状态触发一次新的同步重绘)。这次重绘发生在当前帧内。 - 然后浏览器才开始绘制,直接画出最终的正确效果。用户看不到中间的错误状态。
- 组件因状态改变而重新渲染,
React Team的建议与总结
“If you’re migrating code from a class component, note that
useLayoutEffectfires in the same phase ascomponentDidMountandcomponentDidUpdate. However, we recommend starting with [useEffect] first and only trying [useLayoutEffect] if it turns out you need it.”
—— React官方文档建议:
- 默认使用
useEffect。它的性能更好,是现代 React Apps的首选。 - 只有在遇到可见问题(如闪烁)时,才考虑将特定的副作用迁移到
useLayoutEffect。 - 绝大多数情况下你不需要用到它。
useEffect能覆盖99%的用例。
简单来说:先尝试用 useEffect,如果发现因为布局和绘制的时序问题导致了视觉上的 bug(尤其是闪烁),再换用 useLayoutEffect。