初学 React 的 Hook 机制,特别是 useEffect
的时候,常常会感到困惑。这篇文章将带你系统梳理 useEffect
的执行机制、依赖项数组的作用,以及如何正确清理副作用。如果你也正在学习 React,希望这篇文章能帮助你少走弯路。
如果有理解不当的地方,还请各位大佬轻声指点,共同进步 😊
🧠 为什么选择函数式编程?
在 JavaScript 中,函数是“一等公民”,这意味着你可以像使用变量一样使用函数:传递、返回、赋值给其他变量。这也为 React 使用函数式组件奠定了基础。
函数就是组件,组件返回 JSX,这就是 React 函数组件的核心思想。
而 React Hooks 就是在这种函数式编程基础上诞生的一组工具,帮助我们在不写类的情况下,也能拥有状态管理和副作用处理的能力。
🔁 useState:管理组件的状态
基本用法:
const [count, setCount] = useState(0)
useState
是一个以use
开头的 Hook(约定),用于在函数组件中添加状态。- 它接收一个初始值(如数字、字符串、对象等)。
- 返回一个数组,包含当前状态值和更新该状态的函数。
示例:
function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>点击次数:{count}</p>
<button onClick={() => setCount(count + 1)}>点我</button>
</div>
)
}
通过 useState
,我们实现了组件内部状态的管理,而且不需要使用类组件中的 this.state
和 setState
。
⚠️ useEffect:处理副作用
什么是副作用?
在 React 中,副作用是指那些不会直接影响组件返回的 JSX 内容,但会对外部环境产生影响的操作,例如:
- 请求数据(调用 API)
- 操作 DOM(如聚焦输入框)
- 设置定时器或监听事件
- 订阅外部资源(如 WebSocket)
这些操作不能直接放在组件函数体中同步执行,否则可能导致渲染混乱或者性能问题。
React 提供了 useEffect
这个 Hook,专门用来管理和处理副作用。清理副作用可以帮助开发者管理资源、避免潜在的内存泄漏,以及确保副作用在适当的时机结束或重新初始化。
基本语法:
useEffect(() => {
// 在这里执行副作用操作
}, [依赖项数组]);
它接受两个参数:
- 一个回调函数,用于定义副作用逻辑。
- 一个依赖项数组,决定了副作用的执行时机。
用法详解:
1. 没有依赖项数组:每次渲染都执行
useEffect(() => {
console.log('组件初次渲染和更新时都会执行');
});
这种写法下,只要组件状态发生变化导致重新渲染,副作用就会再次执行。
2. 空数组 []
:仅在组件挂载后执行一次
useEffect(() => {
console.log('仅在组件初次渲染时执行');
}, []);
这是最常见的初始化场景,例如请求数据、订阅全局事件等。
3. 指定依赖项:当依赖项变化时才重新执行
const [count, setCount] = useState(0);
useEffect(() => {
console.log('count 发生变化时执行');
}, [count]);
只有当 count
的值发生变化时,这个副作用才会被触发。
📌 总结三种模式:
依赖项类型 | 是否执行副作用 | 触发时机 |
---|---|---|
不传依赖项 | ✅ 每次 | 组件每次重新渲染 |
空数组 [] | ✅ 一次 | 组件首次挂载 |
指定依赖项数组 | ✅ 条件执行 | 当依赖项发生改变时 |
🧹 清理副作用
有些副作用如果不及时清理,可能会造成内存泄漏或程序异常,例如:
- 定时器未清除 → 即使组件卸载,还在继续执行
- 事件监听器未移除 → 多次注册,响应重复
- 异步请求未取消 → 组件已卸载,结果却试图更新状态
为此,useEffect
允许你在副作用函数中返回一个清理函数,该函数会在以下两种情况下自动执行:
- 组件即将卸载时
- 当前副作用再次执行前(如果依赖项发生了变化)
✅ 示例:清理定时器
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器运行中...');
}, 1000);
return () => {
clearInterval(timer);
console.log('定时器已清除');
};
}, []);
在这个例子中:
- 组件挂载后启动定时器。
- 组件卸载前,清理函数自动执行,清除定时器。
应用场景示例
场景一:组件挂载时获取数据(空依赖数组)
function App() {
const [list, setList] = useState([]);
useEffect(() => {
async function getData() {
const res = await fetch('http://example.cn');
const jsonRes = await res.json();
setList(jsonRes.data.channels);
}
getData();
}, []);
return (
<ul>
{list.map(item => (
<li key={item.id}>{item.name}</li>
))}
</ul>
);
}
📌 特点:只在组件首次渲染时请求数据,适合静态初始化。
场景二:根据状态变化动态获取数据(带依赖项)
const [stateValue, setStateValue] = useState(0);
useEffect(() => {
getData(stateValue); // 每次 stateValue 变化时执行
}, [stateValue]);
📌 特点:当 stateValue
改变时,重新请求数据,适合动态加载场景。
场景三:监听窗口大小变化并打印状态(闭包陷阱)
function App() {
const [count, setCount] = useState(0);
useEffect(() => {
const handle = () => {
console.log(`当前计数值: ${count}`);
};
window.addEventListener('resize', handle);
return () => {
window.removeEventListener('resize', handle);
};
}, [count]);
return (
<button onClick={() => setCount(count + 1)}>增加计数</button>
);
}
📌 注意:如果不在依赖项中加入 count
,那么 handle
函数捕获的是旧的 count
值(闭包陷阱)。添加 [count]
后,确保每次点击按钮都能拿到最新的值。
⚠️ 注意事项与最佳实践
1. 依赖项要完整
如果你在副作用中使用了某个状态或属性,一定要将其加入依赖项数组,否则可能引用到旧值,导致逻辑错误。
❌ 错误示例:
useEffect(() => {
const handle = () => {
console.log(count); // 可能是旧值
};
window.addEventListener('resize', handle);
}, []); // 忘记添加 count
✅ 正确做法:
useEffect(() => {
const handle = () => {
console.log(count); // 永远是最新的值
};
window.addEventListener('resize', handle);
return () => {
window.removeEventListener('resize', handle);
};
}, [count]); // 添加依赖项
2. 避免不必要的依赖项
虽然依赖项要完整,但也应尽量减少依赖项数量,以降低副作用执行频率,提升性能。
可以考虑使用 useRef
缓存某些值,或者使用 useCallback
包裹函数,防止因函数地址变化而触发副作用。
3. 清理副作用不是必须的
并不是每个副作用都需要清理。比如简单的数据请求就不需要,因为请求完成就结束了。但如果涉及定时器、事件监听器、长连接等,则必须清理。
📚 结语
useEffect
是 React 函数组件中最强大也是最容易出错的 Hook 之一。掌握它的执行机制和清理方式,不仅能写出更健壮的代码,也能帮助你更好地理解 React 的生命周期模型。