useEffect 完整使用指南:依赖数组、闭包陷阱、清理函数实战
useEffect 是 React 中最核心、也最容易让人困惑的 Hook 之一。它用于处理副作用:数据获取、订阅、手动修改 DOM、计时器等。
本文将系统性地讲解 useEffect 的工作原理,并重点拆解依赖数组、闭包陷阱、清理函数这三大难点,配合实战示例,帮助你真正掌握它。
一、useEffect 是什么?
在函数组件中,没有 componentDidMount、componentDidUpdate 这些生命周期了。useEffect 就是用来替代它们的。
副作用(Side Effect) :一切"影响组件外部"的操作,比如:
- 请求接口
- 操作 DOM
- 设置定时器
- 订阅事件
基本语法
scss
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)
};
}, [dependencies]);
二、useEffect 的执行时机
理解 useEffect,首先要理解它的执行时机。
javascript
function App() {
console.log('render');
useEffect(() => {
console.log('effect');
});
return <div>App</div>;
}
执行顺序:
render- DOM 更新完成
effect
✅ useEffect 是异步执行的,不会阻塞浏览器渲染。
三、依赖数组:控制 Effect 的触发
useEffect 的第二个参数------依赖数组 ,决定了 effect 何时重新执行。
1️⃣ 不传依赖数组(每次渲染都执行)
javascript
useEffect(() => {
console.log('每次渲染都执行');
});
⚠️ 等价于 componentDidUpdate + componentDidMount,非常容易导致性能问题。
2️⃣ 空依赖数组(只执行一次)
scss
useEffect(() => {
console.log('组件挂载时执行一次');
}, []);
✅ 等价于 componentDidMount
常用于:
- 页面初始化请求
- 初始化第三方库
3️⃣ 有依赖的数组(依赖变化时执行)
scss
useEffect(() => {
console.log('count changed:', count);
}, [count]);
✅ 等价于 componentDidUpdate(但只监听 count)
四、依赖数组的常见误区
❌ 误区一:依赖写少了
scss
useEffect(() => {
setCount(count + 1);
}, []); // ❌ 忘记写 count
React 会警告你,因为 count 被使用了,却没加入依赖。
✅ 正确写法:
scss
useEffect(() => {
setCount(count + 1);
}, [count]);
✅ 原则:effect 中用到的所有响应式值,都必须放进依赖数组
❌ 误区二:依赖写多了
scss
useEffect(() => {
fetchData();
}, [fetchData]);
如果 fetchData 每次渲染都重新创建,effect 就会无限执行。
✅ 解决方案:使用 useCallback
scss
const fetchData = useCallback(() => {
// ...
}, []);
useEffect(() => {
fetchData();
}, [fetchData]);
五、闭包陷阱(最常见、最隐蔽)
这是 useEffect 最让人头疼的问题。
问题示例
javascript
function Counter() {
let count = 0;
useEffect(() => {
const timer = setInterval(() => {
console.log(count); // 永远是 0
}, 1000);
}, []);
return (
<button onClick={() => count++}>
count++
</button>
);
}
为什么?
useEffect在第一次渲染时执行- 它捕获了当时的
count = 0 - 后续点击更新的是新的闭包
- 定时器里的
count永远不会变
✅ 解决方案一:使用 useState
scss
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(count);
}, 1000);
return () => clearInterval(timer);
}, [count]);
✅ 每次 count 变化,effect 重新执行,拿到最新值。
✅ 解决方案二:使用 useRef(获取最新值)
ini
const countRef = useRef(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => clearInterval(timer);
}, []);
const increment = () => {
countRef.current++;
};
✅ 适合:不希望触发重新渲染,但需要访问最新值。
六、清理函数(Cleanup Function)
清理函数用于防止内存泄漏,在以下时机执行:
- 组件卸载时
- effect 重新执行前
基本示例:清除定时器
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('running');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
示例:取消事件监听
javascript
useEffect(() => {
const handler = () => console.log('resize');
window.addEventListener('resize', handler);
return () => {
window.removeEventListener('resize', handler);
};
}, []);
⚠️ 注意:清理函数不是"卸载时才执行"
javascript
useEffect(() => {
console.log('effect');
return () => console.log('cleanup');
}, [count]);
执行顺序:
- cleanup(上一次)
- render
- effect(这一次)
七、useEffect 的实战场景
1️⃣ 数据请求(最常用)
ini
useEffect(() => {
let ignore = false;
fetch('/api/user')
.then(res => res.json())
.then(data => {
if (!ignore) {
setUser(data);
}
});
return () => {
ignore = true; // 防止竞态问题
};
}, [userId]);
✅ 防止组件卸载后 setState 报错。
2️⃣ 同步 URL 参数
ini
useEffect(() => {
const params = new URLSearchParams(window.location.search);
const q = params.get('q');
setKeyword(q);
}, []);
3️⃣ 页面标题修改
ini
useEffect(() => {
document.title = `当前计数:${count}`;
}, [count]);
八、useEffect vs useLayoutEffect
| 对比项 | useEffect | useLayoutEffect |
|---|---|---|
| 执行时机 | 渲染后(异步) | DOM 更新后、浏览器绘制前 |
| 会阻塞渲染 | ❌ | ✅ |
| 使用场景 | 大多数副作用 | 测量 DOM、同步布局 |
✅ 99% 的场景用 useEffect 就够了
九、useEffect 使用原则(总结)
✅ 原则一:effect 中用到的所有变量,必须加入依赖数组
✅ 原则二 :能用 [] 就不用 [deps]
✅ 原则三:清理函数一定要写,防止内存泄漏
✅ 原则四:避免不必要的 effect(能用派生状态就不用 effect)
✅ 原则五:不要把 effect 当"生命周期"用,而要当成"同步副作用"
十、一句话总结
useEffect的本质不是"执行副作用",而是把组件内部的 state / props 与外部系统同步。
当你从"同步"的角度去理解 useEffect,闭包陷阱、依赖数组、清理函数都会变得清晰自然。