React useEffect 内部原理
useEffect 执行时机、依赖数组、清理函数、常见陷阱与 StrictMode 行为
#type / concept
#status / growing
#tech / dev / frame
React useEffect 内部原理
另见 React useEffect 和 React useEffect 与 SSR。
执行时机
useEffect 在浏览器完成绘制 (paint) 之后异步执行,不阻塞渲染:
render → DOM 更新 → 浏览器绘制 → useEffect 回调执行
useEffect(() => {
// 这里的代码在浏览器绘制之后执行
// 适合:数据获取、订阅、手动 DOM 操作
fetchData();
}, []);
依赖数组对比
// 1. 无依赖数组:每次渲染后都执行
useEffect(() => {
console.log('每次渲染后执行');
});
// 2. 空数组:仅挂载时执行一次
useEffect(() => {
console.log('仅挂载时执行');
return () => console.log('卸载时清理');
}, []);
// 3. 有依赖:依赖变化时执行
useEffect(() => {
console.log(`count 变为 ${count}`);
return () => console.log('清理上一次 effect');
}, [count]);
清理函数
useEffect(() => {
const subscription = someAPI.subscribe(handleData);
// 清理函数:在下一次 effect 执行前或卸载时调用
return () => {
subscription.unsubscribe();
};
}, []);
清理函数的执行顺序:
- 运行新的 effect 之前,先执行上一次 effect 的清理
- 组件卸载时,执行当前 effect 的清理
常见陷阱
缺少依赖
// 错误:count 未在依赖数组中,闭包捕获的是初始值
useEffect(() => {
const timer = setInterval(() => {
setCount(count + 1); // count 永远是 0
}, 1000);
return () => clearInterval(timer);
}, []); // 缺少 count
// 正确:使用函数式更新或添加依赖
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1); // 不依赖外部 count
}, 1000);
return () => clearInterval(timer);
}, []);
陈旧闭包 (Stale Closure)
function ChatRoom({ roomId }) {
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect();
}, [roomId]); // roomId 变化时重新连接
}
useEffect vs useLayoutEffect
| 特性 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 浏览器绘制之后 | DOM 变更后、绘制之前 |
| 阻塞渲染 | 否 | 是 |
| 适用场景 | 数据获取、订阅、日志 | 读取 DOM 布局、同步测量 |
useLayoutEffect(() => {
// 在浏览器绘制前测量 DOM,避免闪烁
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height);
}, []);
StrictMode 双重调用
开发模式下,React StrictMode 会故意双重调用 effect 来暴露问题:
挂载 → 执行 effect → 清理 → 再次执行 effect
这意味着 effect 必须是幂等的(执行多次结果相同)。生产环境不会双重调用。
// 好:幂等操作
useEffect(() => {
const conn = createConnection(roomId);
conn.connect();
return () => conn.disconnect(); // 清理函数确保可重复执行
}, [roomId]);
// 差:非幂等(追加 DOM 元素而不清理)
useEffect(() => {
const div = document.createElement('div');
document.body.appendChild(div); // 每次调用都会追加!
// 缺少清理:return () => div.remove();
}, []);