useState实现原理
一句话概括
useState 是 React 函数组件的状态基石——把状态存在 Fiber 节点的单向链表(memoizedState)上,通过环形链表(updateQueue)管理更新,dispatch 触发后走调度→协调→渲染链路完成状态刷新。本质上 useState 就是 useReducer 的语法糖。
核心知识点
1. Hooks 存在哪?Fiber 的 memoizedState 链表
每个函数组件对应一个 Fiber 节点,fiber.memoizedState 指向一个 Hooks 单向链表:
1
2
fiber.memoizedState
→ hook1 (useState) → hook2 (useEffect) → hook3 (useRef) → null
组件每调用一次 Hook,就在链表末尾追加一个节点。这就是为什么 Hooks 的调用顺序必须稳定——链表没有 key,纯靠顺序匹配。
1
2
3
4
5
6
// 简化版 Hook 数据结构
interface Hook {
memoizedState: any // 当前状态值
queue: UpdateQueue // 更新队列(环形链表)
next: Hook | null // 指向下一个 Hook
}
2. 更新队列——环形链表
dispatch 每次调用不会立即更新,而是创建一个 Update 对象插入到 queue.pending 环形链表中:
1
2
3
4
5
6
7
8
9
10
11
12
// Update 对象
{ action: newValue | (prev) => newValue, next: Update | null }
// 插入链表(始终在末尾追加)
const update = { action, next: null }
if (queue.pending === null) {
update.next = update // 自己指向自己形成环
} else {
update.next = queue.pending.next
queue.pending.next = update
}
queue.pending = update
圆形链表的好处:追加 O(1),遍历时从 pending.next 开始走一圈回到 pending 为止,不会丢更新。
3. useState 的简化实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
let currentFiber = null // 当前正在渲染的 Fiber
let hookIndex = 0 // 当前 Hook 在链表中的位置
function useState(initial) {
const fiber = currentFiber
const hook = getOrCreateHook(fiber, hookIndex)
// 执行所有 pending 更新
if (hook.queue.pending) {
let update = hook.queue.pending.next // 环的起点
do {
hook.memoizedState = typeof update.action === 'function'
? update.action(hook.memoizedState)
: update.action
update = update.next
} while (update !== hook.queue.pending.next)
hook.queue.pending = null
}
const dispatch = (action) => {
const update = { action, next: null }
const pending = hook.queue.pending
if (!pending) {
update.next = update
} else {
update.next = pending.next
pending.next = update
}
hook.queue.pending = update
scheduleUpdate(fiber) // 触发重新渲染
}
hookIndex++
return [hook.memoizedState, dispatch]
}
4. 批量更新(Batching)
React 18 之前只在事件处理函数中批量合并 setState;React 18 引入自动批处理,setTimeout、Promise.then 里的多次 setState 也会合并为一次渲染。
1
2
3
4
5
6
7
// React 18 —— 自动批处理
setTimeout(() => {
setCount(c => c + 1) // 不触发渲染
setCount(c => c + 1) // 不触发渲染
setFlag(true) // 不触发渲染
// 到这里只触发一次渲染,count +2 且 flag 更新
}, 1000)
实现原理:React 内部维护一个 isBatchingUpdates 标志,为 true 时 dispatch 只入队不调度;事件/异步回调结束后统一 flush。
5. 为什么说 useState = useReducer 的语法糖
1
2
3
4
5
6
7
8
// useReducer
const [state, dispatch] = useReducer(reducer, initialState)
// useState 等价于
const [state, dispatch] = useReducer(
(prev, action) => typeof action === 'function' ? action(prev) : action,
initialState
)
useState 内部就是调用 useReducer 并传入一个基本的更新函数——函数式更新和值更新都被当作 action 处理,只不过 useState 的 reducer 多了一步判断 action 是函数还是值。
「其实你每天都在用」
- 表单输入绑定:
const [value, setValue] = useState('')+<input onChange={e => setValue(e.target.value)} />。 - 开关/弹窗状态:
const [visible, setVisible] = useState(false),打开/关闭。 - 请求三态:
data/loading/error三个 useState 或一个{data, loading, error}对象。 - 列表选中项:
const [selected, setSelected] = useState<Set<string>>(new Set()),复选框交互。 - 函数式更新解决闭包陷阱:
setCount(prev => prev + 1)在依赖闭包场景确保取到最新值。
常见误解(FAQ)
❌ 误区:多次 setState 会触发多次渲染。 React 18 已自动批处理,同一个回调里的多次 setState 合并为一次渲染(包括 setTimeout 和 Promise 里)。React 17 只有事件处理函数里会批处理。
❌ 误区:useState 的更新是异步的。 本质是同步的——dispatch(action) 执行时立即创建 update 入队。只是 React 不立即渲染,而是等当前代码块执行完后在微任务/同步批处理时机才 flush。所以拿到的 state 是「下一次渲染」的,不是「立即拿到」。
❌ 误区:setState 传函数和传值效果一样。 函数式更新 setCount(prev => prev + 1) 拿到的是队列中的最新值而非闭包中的旧值。如果你在同一个事件里调了 3 次 setCount(count + 1),count 闭包值不变,结果只 +1。用函数式更新才是每次基于最新值 +1。
❌ 误区:useState 初始化只执行一次。 严格模式下(StrictMode),React 在开发环境会 double-invoke 初始化和更新函数来帮你发现副作用问题。所以 useState(() => expensive()) 在开发模式可能跑两次,生产环境只跑一次。不要依赖初始化只执行一次。
一句话总结
useState 的本质就是把一个带更新队列的变量挂在了 Fiber 节点的链表上——理解了「链表存状态」和「环形链表存更新」,就理解了 React Hooks 一半的秘密。