在 React 的函数组件世界里,useEffect
堪称处理副作用的 "瑞士军刀"。它能让我们优雅地实现数据获取、定时器管理、事件监听等操作,还能无缝衔接类组件的生命周期逻辑。今天,我们就来全方位拆解这个核心 Hook,让你从 "会用" 进阶到 "精通"!
一、useEffect 基础:揭开副作用的神秘面纱
1. 什么是副作用?
简单来说,副作用就是组件渲染之外的 "额外操作" ,比如:
-
发起网络请求获取数据
-
设置定时器或 Interval
-
添加 / 移除事件监听
-
手动操作 DOM
这些操作不能直接写在组件函数里(会阻塞渲染),而useEffect
就是 React 提供的 "副作用专属容器"。
2. 基本语法与执行时机
javascript
useEffect(() => {
// 副作用逻辑(如数据获取、事件监听等)
console.log('副作用执行');
return () => {
// 清理函数(组件卸载或更新前执行)
console.log('清理副作用');
};
}, [依赖数组]); // 可选,控制副作用何时重新执行
-
无依赖数组 :每次组件渲染(挂载 + 更新)后都会执行,相当于
componentDidMount
+componentDidUpdate
-
空依赖数组
[]
:仅在组件挂载后执行一次,类似componentDidMount
-
指定依赖项 :只有依赖项变化时才执行,比如
[count]
表示count
状态变化时触发
💡 小提醒:React 会在浏览器完成页面渲染后异步执行useEffect
,不会阻塞用户界面,这点和useLayoutEffect
的同步执行不同哦~
二、生命周期平替:useEffect 的 "三重身份"
1. 挂载阶段:模拟 componentDidMount
当依赖数组为空时,useEffect
会在组件首次渲染后执行,适合做初始化操作:
javascript
useEffect(() => {
console.log('组件挂载完成!');
// 发起初始化数据请求
fetchData();
}, []);
2. 更新阶段:替代 componentDidUpdate
当依赖数组包含特定状态 / Props 时,只有它们变化才会触发副作用:
javascript
const [count, setCount] = useState(0);
useEffect(() => {
console.log(`count更新为:${count}`);
}, [count]); // 仅count变化时执行
3. 卸载阶段:实现 componentWillUnmount
通过返回清理函数,在组件卸载前执行资源释放操作:
javascript
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => {
clearInterval(timer); // 清除定时器,避免内存泄漏
console.log('组件卸载,定时器已清除');
};
}, []);
🎯 关键点:清理函数会在组件卸载时执行,也会在下次同 effect 执行前执行,确保副作用 "有始有终"。
三、实战场景:用 useEffect 解决真实问题
1. 数据获取:接口请求的正确姿势
❌ 错误示范(直接用 async)
javascript
// 警告!useEffect不能直接返回Promise
useEffect(async () => {
const data = await fetchData();
setData(data);
}, []);
✅ 正确做法(内部定义 async 函数)
javascript
useEffect(() => {
const fetchData = async () => {
const response = await fetch('https://api.example.com/data');
const result = await response.json();
setData(result);
};
fetchData(); // 立即执行异步函数
}, []); // 空依赖确保仅挂载时请求
2. 事件监听:动态绑定与解绑
javascript
const [windowWidth, setWindowWidth] = useState(window.innerWidth);
useEffect(() => {
const handleResize = () => {
setWindowWidth(window.innerWidth);
};
window.addEventListener('resize', handleResize); // 挂载时绑定事件
return () => {
window.removeEventListener('resize', handleResize); // 卸载时解绑
};
}, []); // 仅绑定/解绑一次,性能更佳
3. 复杂场景:多个 effect 拆分关注点
javascript
function UserProfile({ userId }) {
const [user, setUser] = useState(null);
const [posts, setPosts] = useState([]);
// 拆分不同副作用,逻辑更清晰
useEffect(() => {
// 获取用户信息
fetchUser(userId).then(setUser);
}, [userId]);
useEffect(() => {
// 获取用户帖子
fetchPosts(userId).then(setPosts);
}, [userId]);
// ... 组件渲染逻辑
}
四、避坑指南:常见问题与最佳实践
1. 依赖数组的 "精准控制"
- 不要遗漏必要依赖 :ESLint 的
react-hooks/exhaustive-deps
规则能帮你检测缺失的依赖项 - 避免冗余依赖:如果函数内部没有使用某个状态 / Props,就不要放进依赖数组
- 使用函数式更新 :当副作用依赖前一次状态时(如
setCount(prev => prev + 1)
),可以省略依赖项
2. 处理异步操作的内存泄漏
在数据请求场景中,组件可能在请求完成前卸载,此时更新状态会导致报错。解决方案:
javascript
useEffect(() => {
let isMounted = true; // 标记组件是否仍挂载
const fetchData = async () => {
const data = await fetchData();
if (isMounted) { // 确保组件未卸载时更新状态
setData(data);
}
};
fetchData();
return () => {
isMounted = false; // 卸载时清除标记
};
}, []);
3. 避免无限循环
当副作用内更新依赖的状态时,可能触发死循环:
javascript
// ❌ 错误:每次effect执行都会更新count,导致无限循环
useEffect(() => {
setCount(count + 1);
}, [count]);
// ✅ 正确:仅初始化时执行一次
useEffect(() => {
setCount(0); // 初始值设置,空依赖避免重复执行
}, []);
五、代码示例:完整组件中的 useEffect 应用
父组件 App.js(数据获取 + 组件卸载清理)
javascript
import { useState, useEffect } from 'react';
import Timer from './Timer';
function App() {
const [repos, setRepos] = useState([]);
const [isTimerOn, setIsTimerOn] = useState(true);
// 仅在挂载时获取GitHub仓库数据
useEffect(() => {
const fetchRepos = async () => {
const response = await fetch('https://api.github.com/users/shunwuyu/repos');
const data = await response.json();
setRepos(data);
};
fetchRepos();
}, []);
return (
<div>
<h2>我的GitHub仓库</h2>
<ul>
{repos.map(repo => (
<li key={repo.id}>{repo.full_name}</li>
))}
</ul>
<h3>定时器演示</h3>
{isTimerOn && <Timer />}
<button onClick={() => setIsTimerOn(!isTimerOn)}>
切换定时器 {isTimerOn ? '关闭' : '开启'}
</button>
</div>
);
}
export default App;
子组件 Timer.js(定时器清理)
javascript
import { useState, useEffect } from 'react';
function Timer() {
const [time, setTime] = useState(0);
useEffect(() => {
const interval = setInterval(() => {
setTime(prev => prev + 1); // 使用函数式更新,避免闭包问题
}, 1000);
return () => {
clearInterval(interval); // 组件卸载时清除定时器
console.log('定时器已清除,避免内存泄漏~');
};
}, []); // 空依赖,仅初始化时启动定时器
return <div>已运行 {time} 秒</div>;
}
export default Timer;
六、总结:useEffect 的核心价值
-
统一生命周期:一个 Hook 搞定挂载、更新、卸载三阶段逻辑
-
精准控制:依赖数组让副作用 "按需执行",避免不必要的性能损耗
-
函数式风格 :配合
useState
等 Hook,让函数组件拥有媲美类组件的能力,代码更简洁易维护
下次遇到副作用场景时,记得想想useEffect
的三个灵魂拷问:
-
这个操作需要在什么时机执行?(挂载 / 更新 / 卸载)
-
哪些变量变化会触发这个副作用?(依赖数组如何定义)
-
是否需要清理资源?(定时器、事件监听、未完成的请求)
掌握这几点,你就能让useEffect
真正成为你的 React 开发好帮手!