普通视图

发现新文章,点击刷新页面。
今天 — 2025年12月21日首页

React Hooks 深度理解:useState / useEffect 如何管理副作用与内存

2025年12月20日 21:32

🤯你以为 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 内部顺序是这样的:

  1. 执行上一次 effect 的 cleanup
  2. 再执行新的 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 不只是语法升级,
而是一场从“命令式生命周期”
到“声明式副作用管理”的革命。

❌
❌