useState 是 React Hooks 中最核心的 Hook 之一。
你可以把它理解成:
"函数组件中的状态存储器 + 更新调度器"
很多人会背:
scss
const [state, setState] = useState(initialValue)
但真正面试或者深入开发时,更重要的是理解:
useState为什么能记住状态?- 为什么
setState后不会立刻更新? - React 为什么会"批量更新"?
- React18 为什么有自动批量更新?
- 为什么连续
setCount(count + 1)不会 +2?
下面系统讲。
一、useState 原理
1. useState 本质
本质上:
ini
useState = 状态存储 + 更新函数
React 内部会为每个组件维护:
fiber.memoizedState
里面像链表一样保存 Hook。
大概类似:
yaml
{
state: 0,
queue: []
}
二、为什么函数组件每次执行还能记住状态?
看起来很奇怪:
javascript
function App() {
const [count, setCount] = useState(0)
return <div>{count}</div>
}
函数每次重新执行:
count 不应该重新变成 0 吗?
其实不会。
因为:
php
状态不在函数里
状态在 React Fiber 对象里
React 会:
perl
组件重新执行
↓
按顺序读取 hooks
↓
拿到上次保存的 state
三、Hook 为什么不能写在 if 里?
因为 React 是:
按调用顺序记录 hooks
例如:
scss
useState() // 第1个hook
useEffect() // 第2个hook
useState() // 第3个hook
React 靠"位置"识别 hook。
如果:
scss
if (flag) {
useState()
}
下一次顺序变了:
hook 对不上
状态错乱。
所以:
Hooks 必须顶层调用
四、setState 为什么是异步的?
例如:
scss
const [count, setCount] = useState(0)
const click = () => {
setCount(1)
console.log(count)
}
输出:
0
不是 1。
因为:
markdown
setState 不会立刻修改 state
而是:
1. 先放入更新队列
2. React 调度更新
3. 重新渲染组件
五、useState 更新流程(核心)
React 内部大概:
scss
setState()
↓
创建 update 对象
↓
放入 queue
↓
调度 render
↓
执行函数组件
↓
计算新 state
↓
更新 DOM
六、为什么连续 setCount 不会累加?
经典题:
scss
const [count, setCount] = useState(0)
const click = () => {
setCount(count + 1)
setCount(count + 1)
}
结果:
1
不是 2。
因为:
两次拿到的:
ini
count === 0
等价于:
scss
setCount(1)
setCount(1)
最终还是 1。
七、正确累加:函数式更新
应该:
ini
setCount(prev => prev + 1)
setCount(prev => prev + 1)
结果:
2
因为 React 会:
ini
第一次:
prev = 0 → 1
第二次:
prev = 1 → 2
八、批量更新机制(重点)
React 为了性能:
不会每次 setState 都立刻 render
而是:
把多个更新合并
最后统一 render 一次
这就是:
Batch Update(批量更新)
九、React17 的批量更新
React17 中:
只有 React 事件内才批量更新。
例如:
scss
const click = () => {
setA(1)
setB(2)
}
只 render 一次。
但:
scss
setTimeout(() => {
setA(1)
setB(2)
})
会 render 两次。
因为:
arduino
setTimeout 不在 React 控制范围
十、React18 自动批量更新
React18:
扩大了批量更新范围。
现在:
javascript
setTimeout
promise
async/await
原生事件
也会自动批量更新。
例如:
scss
setTimeout(() => {
setA(1)
setB(2)
})
React18:
只 render 一次
十一、React18 为什么能做到?
React18 引入:
Concurrent Features(并发特性)
内部调度器更强。
React 能统一收集更新:
微任务阶段统一处理
所以实现了:
Automatic Batching
十二、什么时候会立即更新?
可以强制同步:
scss
flushSync(() => {
setCount(1)
})
来自:
javascript
import { flushSync } from 'react-dom'
这会:
立即 render
但一般少用。
十三、useState 底层数据结构(面试加分)
React Hook 内部:
单向链表
Fiber 中:
fiber.memoizedState
每个 Hook 节点:
yaml
{
memoizedState: 当前状态,
queue: 更新队列,
next: 下一个hook
}
十四、简化版 useState 实现(理解核心)
可以自己模拟:
scss
let state
function useState(initialValue) {
state = state || initialValue
function setState(newValue) {
state = newValue
render()
}
return [state, setState]
}
真正 React 当然复杂很多:
- Fiber
- Hook 链表
- 优先级
- 更新队列
- 调度器
- 并发更新
但核心思想类似。
十五、面试高频题总结
1. useState 是同步还是异步?
答:
setState 本身不是异步 API
而是 React 的状态更新是"调度式"的
表现上像异步
2. 为什么 state 不立刻更新?
因为:
React 要做批量更新优化
3. 为什么 Hook 不能放 if?
因为:
React 按 hook 调用顺序记录状态
4. 为什么 useState 能记住状态?
因为:
php
状态存在 Fiber,不在函数里
5. React18 批量更新变化?
React18:
javascript
自动批量更新扩大到:
setTimeout
Promise
async/await
原生事件
十六、真正理解一句话(最重要)
可以记住:
React 不是"调用 setState 修改变量"
而是:
提交一个更新任务给 React 调度器
这是理解 React 更新机制的核心。
十七、完整流程图(非常重要)
arduino
点击按钮
↓
setState
↓
创建 update
↓
放入 queue
↓
React Scheduler 调度
↓
render 阶段
↓
重新执行函数组件
↓
生成新的 Virtual DOM
↓
diff
↓
commit
↓
更新真实 DOM
十八、一个经典面试题
scss
function App() {
const [count, setCount] = useState(0)
console.log("render")
return (
<button
onClick={() => {
setCount(count + 1)
setCount(count + 1)
Promise.resolve().then(() => {
setCount(count + 1)
})
}}
>
{count}
</button>
)
}
React18 下点击后:
最终:
ini
count = 1
因为:
所有拿到的 count 都是:
当前 render 快照里的 0
如果改成:
ini
setCount(c => c + 1)
最终:
ini
count = 3
这就是:
"闭包快照" + "批量更新"
的核心。