你以为它只是替代componentDidMount
?数据抓取、事件绑定、定时清理...?事实上,useEffect
才是函数组件的"幕后操控者"!但依赖数组的坑、闭包的陷阱,你真的玩转了吗?
告别"能用就行",今天带你彻底拆解核心逻辑,从优雅使用到精准避坑,解锁真正的"精通"段位!
一、useEffect 基础:揭开副作用这层神秘的面纱
简单来说,副作用就是组件渲染之外的 "额外操作" ,比如:
-
发起网络请求获取数据
-
设置定时器或 Interval
-
添加 / 移除事件监听
-
手动操作 DOM
这些操作不能直接写在组件函数里(会阻塞渲染),而useEffect
就是 React 提供的 "副作用专属容器"。
- 基本语法与执行时机制
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(() => {
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
// 解决方案:仅初始化时执行一次
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 核心利器。