在 React 的演进史中,Hooks 的出现无疑是一次范式转移。然而,许多开发者至今仍将其视为 Class 组件生命周期的"语法糖",仅仅是用 useEffect 去模拟 componentDidMount,用 useState 去替代 this.state。
这种理解方式不仅肤浅,而且危险。它忽略了 Hooks 设计背后的核心哲学:函数式编程与代数效应。
在函数组件的世界里,渲染是一个纯粹的计算过程:UI = f(State)。然而,现实世界充满了不确定性与副作用。本文将深入剖析 useState 与 useEffect,带你重塑关于"状态记忆"与"副作用管理"的心智模型。
一、useState ------ 不仅仅是变量赋值
在 JavaScript 函数中,普通的局部变量(如 let count = 0)是短暂的。函数执行完毕,变量即被回收;函数再次执行,变量重置为初始值。
useState 的本质,是赋予纯函数组件"记忆"的能力。它让数据在多次渲染(函数调用)之间得以持久化。
1. 深度特性:惰性初始化 (Lazy Initialization)
我们习惯于这样初始化状态:
JavaScript
scss
const [count, setCount] = useState(0);
但如果初始值需要经过昂贵的计算得出,例如读取 LocalStorage 并解析 JSON,或者进行复杂的数学运算:
JavaScript
scss
// 错误示范:每次 Render 都会执行 expensiveComputation
const [state, setState] = useState(expensiveComputation());
虽然 React 只在首次渲染时使用该初始值,但 expensiveComputation() 函数本身在每一次组件重新渲染时都会被执行。这是一种严重的性能浪费。
比方来说:这就好比你每次回家(Render),都要把房子重新装修一遍(执行计算),但实际上你只需要在第一次搬进来时装修(初始化)。
最佳实践 是将函数本身传递给 useState,这就是惰性初始化:
JavaScript
scss
// 正确示范:React 只在首次 Render 时调用该函数
const [state, setState] = useState(() => {
return expensiveComputation();
});
注意:初始化函数必须是同步的纯函数,不能包含异步操作,因为 React 需要在渲染阶段立即确定状态的初值。
2. 深度特性:函数式更新 (Functional Updates)
在处理异步更新或闭包场景时,直接传递新值往往是 Bug 的源头:
JavaScript
scss
const [count, setCount] = useState(0);
// 在某些闭包场景下,count 可能是旧值(Stale State)
setCount(count + 1);
React 推荐使用函数式更新:
JavaScript
ini
setCount(prevCount => prevCount + 1);
比方来说:
- 直接更新 setCount(count + 1) 就像是拿着一张旧照片去画新画,如果照片过时了,画出来的结果就是错的。
- 函数式更新 setCount(prev => prev + 1) 就像是在当前的画布上直接添上一笔,无论之前的状态如何流转,它总是基于最新的真实数据。

二、useEffect ------ 驯服副作用的艺术
函数组件的核心应当是纯函数:给定相同的 Props 和 State,永远返回相同的 JSX,且不产生任何副作用(Side Effects)。
然而,应用需要与外部世界交互:网络请求、DOM 操作、订阅事件、定时器。这些都是"副作用"。useEffect 不是生命周期钩子,它是副作用的隔离区 ,或者是同步 React 状态与外部系统的桥梁。
依赖数组 (Dependency Array) 的本质
许多初学者死记硬背:[] 是 Mount,[prop] 是 Update。这种理解是片面的。
依赖数组本质上是一个同步闸门。React 在每次渲染后,都会询问:"这个副作用所依赖的数据变了吗?"
- 不传依赖:闸门大开,每次渲染都执行。
- 空数组 [] :闸门永久关闭(除首次外),副作用只执行一次。
- 指定依赖 [dep] :仅当 dep 发生浅比较变化时,闸门开启。
比方来说 :依赖数组就像机场的安检门。只有当你携带的"行李"(依赖项)发生变化时,安检人员(React)才允许你通过并执行后续流程(副作用)。如果行李没变,你就可以直接跳过安检。
三、Cleanup ------ 遗忘的角落与内存泄漏
在 useEffect 中返回的函数被称为清理函数 (Cleanup Function) 。它是防止内存泄漏的关键。
JavaScript
javascript
useEffect(() => {
const timer = setInterval(() => console.log('Tick'), 1000);
return () => clearInterval(timer); // Cleanup
}, [dependency]);
执行时机解密
这是一个常见的误区:认为清理函数只在组件卸载(Unmount)时执行。
事实是:清理函数会在下一次副作用执行前被调用。
比方来说 :这就像借书与还书的规则。
- 你借了一本新书(执行新的 Effect)。
- 当你想要借下一本书之前,图书馆规定你必须先归还上一本书(执行 Cleanup)。
- 当然,当你彻底离开图书馆(组件卸载)时,也必须归还手里所有的书。
如果忽略清理函数,由于闭包的存在,旧的副作用(如定时器)会引用旧的变量,并在后台持续运行,导致严重的内存泄漏和逻辑错误。

四、实战剖析 (Code Analysis)
结合实际代码,我们来观察状态流转与副作用清理的全过程。以下代码包含父组件 App 和子组件 Demo。
JavaScript
javascript
// App.jsx
import { useState, useEffect } from 'react';
import Demo from './components/Demo.jsx';
export default function App() {
const [num, setNum] = useState(0);
useEffect(() => {
// 副作用:开启定时器
const timer = setInterval(() => {
console.log(`Current Num: ${num}`);
}, 1000)
// 清理:清除上一次的定时器
return () => {
console.log('App Cleanup: remove timer');
clearInterval(timer);
}
}, [num]) // 依赖项:num
return (
<>
<div onClick={() => setNum(prev => prev + 1)}>
Num: {num} (Click to increment)
</div>
{/* 条件渲染:偶数显示 Demo,奇数卸载 Demo */}
{num % 2 === 0 && <Demo />}
</>
)
}
JavaScript
javascript
// Demo.jsx
import { useEffect } from 'react';
export default function Demo() {
useEffect(() => {
console.log('Demo Mounted');
const timer = setInterval(() => {
console.log('Demo Timer Tick');
}, 1000)
return () => {
console.log('Demo Unmounted -> Cleanup');
clearInterval(timer);
}
}, []) // 空依赖:只在挂载和卸载时执行
return <div>偶数 Demo 组件</div>
}
场景一:父组件的状态变化
当用户点击 div 使 num 从 0 变为 1 时:
- React 渲染:App 组件重新执行,num 变为 1。
- 依赖检查:useEffect 发现依赖项 [num] 发生变化(0 -> 1)。
- 执行清理 :首先执行上一次 Effect 返回的函数。控制台打印 App Cleanup: remove timer,销毁了引用 num=0 的旧定时器。
- 执行新副作用 :执行本次 Effect。开启一个新的定时器,该定时器闭包中引用的是 num=1。
结论:正是这个"先清理,后执行"的机制,保证了控制台打印的永远是当前最新的 num,且系统中同一时刻只有一个活跃的 App 定时器。
场景二:子组件的挂载与卸载
当 num 从 0 变为 1:
- 条件 num % 2 === 0 为 false。
- React 决定将 从 DOM 中移除。
- 触发卸载清理:React 自动调用 Demo 组件内部 useEffect 的清理函数。控制台打印 Demo Unmounted -> Cleanup,Demo 内部的定时器被清除。
当 num 从 1 变为 2:
- 条件为 true, 重新被创建并挂载。
- 触发挂载副作用:控制台打印 Demo Mounted,开启新的定时器。
五、结语
React Hooks 的精髓在于声明式编程。
在使用 Class 组件时,我们习惯于命令式地思考:"在 DidMount 里做这个,在 DidUpdate 里做那个,在 WillUnmount 里清理这个"。
而在 Hooks 的世界里,你需要转换思维:
- 告诉 React 状态 (State) 是什么。
- 告诉 React 副作用 (Effect) 应该如何与状态同步。
- 告诉 React 当同步不再需要时,如何回滚 (Cleanup) 。
useState 提供了不可变数据的快照,useEffect 提供了数据变化时的响应机制。只有深刻理解了闭包、纯函数与副作用的边界,才能真正写出健壮、优雅且无内存泄漏的 React 代码。