React Hooks 深度理解:useState / useEffect 如何管理副作用与内存
🤯你以为 React Hooks 只是语法糖?
不——它们是在帮你对抗「副作用」和「内存泄漏」
如果你只把 Hooks 当成“不用 class 了”,
那你可能只理解了 React 的 10%。
🚀 一、一个“看起来毫无问题”的组件
我们先从一个你我都写过无数次的组件开始:
function App() {
const [num, setNum] = useState(0)
return (
<div onClick={() => setNum(num + 1)}>
{num}
</div>
)
}
看起来非常完美:
- ✅ 没有 class
- ✅ 没有 this
- ✅ 就是一个普通函数
但问题是:
React 为什么要发明 Hooks?
useState / useEffect 到底解决了什么“本质问题”?
答案其实只有一个关键词👇
💣 二、React 世界的终极敌人:副作用(Side Effect)
React 背后有一个很少被明说,但极其重要的信仰:
组件 ≈ 纯函数
🧠 什么是纯函数?
- 相同输入 → 永远相同输出
- 不依赖外部变量
- 不产生额外影响(I/O、定时器、请求)
function add(a, b) {
return a + b
}
而理想中的 React 组件是:
(props + state) → JSX
React 希望你“只负责算 UI”,
而不是在渲染时干别的事。
⚠️ 但现实是:你必须干“坏事”
真实业务中,你不可避免要做这些事:
- 🌐 请求接口
- ⏱️ 设置定时器
- 🎧 事件监听
- 📦 订阅 / 取消订阅
- 🧱 操作 DOM
这些行为有一个共同点👇
❌ 它们都不是纯函数行为
✅ 它们都是副作用
如果你直接把副作用写进组件函数,会发生什么?
function App() {
fetch('/api/data') // ❌
return <div />
}
👉 每一次 render 都请求
👉 状态更新 → 再 render → 再请求
👉 组件直接失控
🧯 三、useEffect:副作用的“隔离区”
useEffect 的存在,本质只干一件事:
把副作用从“渲染阶段”挪走
useEffect(() => {
// 副作用逻辑
}, [])
💡 一句话理解:
render 阶段必须纯,
effect 阶段允许脏。
📦 四、依赖数组不是细节,而是“副作用边界”
1️⃣ 只执行一次(挂载)
useEffect(() => {
console.log('mounted')
}, [])
- 只在组件挂载时执行
- 类似 Vue 的
onMounted
2️⃣ 依赖变化才执行
useEffect(() => {
console.log(num)
}, [num])
-
num变化 → 执行 - 不变 → 不执行
依赖数组的本质是:
“这个副作用依赖谁?”
3️⃣ 不写依赖项?
useEffect(() => {
console.log('every render')
})
👉 每次 render 都执行
👉 99% 的时候是性能陷阱
💥 五、90% 新手都会踩的坑:内存泄漏
来看一个极其经典的 Hooks 错误写法👇
useEffect(() => {
const timer = setInterval(() => {
console.log(num)
}, 1000)
}, [num])
你觉得这段代码有问题吗?
有,而且非常致命。
❌ 问题在哪里?
-
num每变一次 - effect 重新执行
- 新建一个定时器
- ❗旧定时器还活着
结果就是:
- ⏱️ 定时器越来越多
- 📈 内存持续上涨
- 💥 控制台疯狂打印
- 🧠 内存泄漏
🧹 六、useEffect return:副作用的“善终机制”
React 给你准备了一个官方清理通道👇
useEffect(() => {
const timer = setInterval(() => {
console.log(num)
}, 1000)
return () => {
clearInterval(timer)
}
}, [num])
⚠️ 重点来了
return 的函数不是“卸载时才执行”
而是:
下一次 effect 执行前,一定会先执行它
React 内部顺序是这样的:
- 执行上一次 effect 的 cleanup
- 再执行新的 effect
👉 这就是 Hooks 防内存泄漏的核心设计
🧠 七、useState:为什么初始化不能异步?
你在学习 Hooks 时,一定问过这个问题👇
❓ 我能不能在 useState 初始化时请求接口?
useState(async () => {
const data = await fetchData()
return data
})
答案很干脆:
❌ 不行
🤔 为什么不行?
因为 React 必须保证:
- 首次 render 立即有确定的 state
- 异步结果是不确定的
- state 一旦初始化,必须是同步值
React 允许的只有这种👇
useState(() => {
const a = 1 + 2
const b = 2 + 3
return a + b
})
💡 这叫 惰性初始化
💡 但前提是:同步 + 纯函数
🌐 八、那异步请求到底该写哪?
答案只有一个地方:
useEffect
useEffect(() => {
async function query() {
const data = await queryData()
setNum(data)
}
query()
}, [])
🎯 这是 React 官方推荐模式
- state 初始化 → 确定
- 异步请求 → 副作用
- 数据回来 → 更新状态
🔄 九、为什么 setState 可以传函数?
setNum(prev => prev + 1)
这不是“花里胡哨”,而是并发安全设计。
React 内部可能会:
- 合并多次更新
- 延迟执行 setState
如果你直接用 num + 1,很可能拿到的是旧值。
函数式 setState = 永远安全
🏁 十、Hooks 的真正价值(总结)
如果你只把 Hooks 当成:
“不用写 class 了”
那你只看到了表面。
Hooks 真正解决的是:
- 🧩 状态如何在函数中稳定存在
- 🧯 副作用如何被精确控制
- 🧠 生命周期如何显式建模
- 🔒 内存泄漏如何被主动规避
✨ 最后的掘金金句
useState 解决的是:数据如何“活着”
useEffect 解决的是:副作用如何“善终”React Hooks 不只是语法升级,
而是一场从“命令式生命周期”
到“声明式副作用管理”的革命。