useState是如何实现的
面试速答(30 秒版 TL;DR)
useState的核心不是“返回一个值和一个函数”,而是 React 如何在多次渲染之间记住这份状态。- 在函数组件里,React 会把 Hooks 挂到当前组件对应的 Fiber 上。
- 对
useState来说,至少要有三样东西:- 当前状态值
- 更新队列
- 固定的 Hook 顺序
setState本质上不是直接改变量,而是:- 先把 action 放进更新队列
- 再调度当前 Fiber 重新渲染
- 下次 render 时按队列依次算出新 state
- 一句话总结:
useState= Fiber 上的 Hook 链表 + 更新队列 + 重新渲染时按调用顺序取回状态。
一、先回答本质问题:状态到底存哪
函数组件每次执行,函数体都会重新跑一遍。
所以 count 这类状态不可能只存在函数局部变量里,否则每次 render 都会丢失。
React 的做法是:
- 每个函数组件对应一个 Fiber 节点
- Fiber 上有一个
memoizedState memoizedState指向一条 Hook 链表
也就是说,函数组件的 Hook 状态真正挂在 组件对应的 Fiber 节点 上。
二、一个 useState Hook 节点里有什么
可以把一个 useState Hook 节点粗略理解成:
type Hook<State> = {
memoizedState: State
baseState: State
queue: UpdateQueue<State> | null
next: Hook<unknown> | null
}
其中最重要的是:
memoizedState:当前这次 render 读到的 statequeue:待处理的更新队列next:下一个 Hook 节点
所以多个 Hooks 并不是存在数组里,而是挂成一条链表。
三、首次渲染时 useState 做了什么
首次渲染通常叫 mount。
这时 React 会:
- 创建一个新的 Hook 节点。
- 把初始值放进
memoizedState。 - 创建更新队列
queue。 - 生成一个稳定的
dispatch函数。 - 把这个 Hook 节点挂到当前 Fiber 的 Hook 链表上。
可以把它简化成下面这种伪代码:
function mountState(initialState) {
const hook = mountWorkInProgressHook()
hook.memoizedState =
typeof initialState === 'function' ? initialState() : initialState
hook.baseState = hook.memoizedState
hook.queue = { pending: null, dispatch: null }
const dispatch = action => {
dispatchSetState(currentFiber, hook.queue, action)
}
hook.queue.dispatch = dispatch
return [hook.memoizedState, dispatch]
}
这里要注意两点:
- 惰性初始化函数只在 mount 时执行
dispatch要和当前 Fiber、当前队列绑定
四、更新时 setState 到底做了什么
很多人会误以为 setState 是“直接把 state 改掉”。
真实过程更接近:
- 创建一个 update 对象。
- 把它塞进该 Hook 的更新队列。
- 给当前更新分配优先级。
- 调度对应 Fiber 重新 render。
也就是说:
setState负责“登记更新”- 真正“算出新 state”发生在下一轮 render
一个 update 可以是:
- 直接值:
setCount(1) - 函数式更新:
setCount(prev => prev + 1)
函数式更新之所以重要,是因为它基于队列里的前一个结果继续算,更适合连续更新场景。
五、下一次 render 时怎么得到新 state
更新阶段通常叫 update。
React 重新执行组件时,会再次按顺序走到 useState,这时不是重新创建,而是:
- 取到旧 Hook 对应的新 Hook 节点。
- 读取它的更新队列。
- 把队列里的 action 按顺序应用到
baseState。 - 计算出新的
memoizedState。 - 返回
[newState, dispatch]。
伪代码可以理解成:
function updateState() {
const hook = updateWorkInProgressHook()
const queue = hook.queue
let newState = hook.baseState
for (const update of consume(queue.pending)) {
newState =
typeof update.action === 'function'
? update.action(newState)
: update.action
}
hook.memoizedState = newState
hook.baseState = newState
return [hook.memoizedState, queue.dispatch]
}
六、为什么 Hooks 必须固定调用顺序
这是 useState 题最容易被追问的地方。
React 在函数组件里拿 Hook,不是按名字,而是按“第几个 Hook 调用”定位。
比如:
- 第一个 Hook 读链表第一个节点
- 第二个 Hook 读第二个节点
- 第三个 Hook 读第三个节点
如果你把某个 Hook 放进条件分支:
- 这次执行有
- 下次执行没有
那么后面的 Hook 就会整体错位。
这也是为什么 React 强调:
- Hooks 只能在顶层调用
- Hooks 不能放在条件、循环、嵌套函数里
七、dispatch 为什么在多次 render 之间还能稳定
因为 dispatch 不是每次调用现算的匿名函数结果,而是 mount 时创建后保存在 Hook 队列里。
它内部闭包会记住:
- 当前组件对应的 Fiber
- 当前 Hook 对应的更新队列
所以你后续点击按钮调用的其实是同一个 dispatch 入口,只是每次入队不同 action。
八、真实 React 比“极简版 useState”多了哪些东西
面试里最好补一句,不然容易显得只会背玩具实现。
真实 React 还会处理:
- 优先级 lanes
- 批处理
- eager state 优化
Object.is比较后的 bailout- base queue / skipped updates
- 并发更新和重放
这也是为什么你手写一个数组版 useState,只能说明原理,不能等同于 React 真正实现。
九、典型题标准答法
问:useState 是如何实现的?
useState的状态不是存在函数局部变量里,而是挂在当前函数组件对应的 Fiber 节点上。Fiber 的memoizedState会指向一条 Hook 链表,每个useStateHook 节点里保存当前 state、更新队列和下一个 Hook。调用setState时,React 不会直接改值,而是把 action 放进队列并调度该 Fiber 重新渲染;下次 render 再按 Hook 调用顺序取到对应节点,把队列里的 action 依次执行,得到新的 state。这也是 Hooks 必须保证调用顺序稳定的原因。
十、常见追问
1. useState 和 class 组件的 setState 最大区别是什么?
useState 更直接对应单个 Hook 的更新队列;class setState 则围绕实例和局部 state 合并逻辑展开,内部模型不同。
2. 为什么 setCount(count + 1) 连续调用两次不一定加 2?
因为闭包里拿到的 count 可能是同一轮 render 的旧值。连续依赖上一次结果时,应优先用函数式更新。
3. 初始值函数为什么只执行一次?
因为惰性初始化只发生在 mount 阶段,update 阶段会直接复用已有 Hook 节点。
十一、易错点 / 坑
- 以为
setState会立刻同步改state变量。 - 以为 Hooks 状态存在组件函数局部变量里。
- 只会讲数组 + 索引版实现,却不知道真实 React 是 Fiber 上的 Hook 链表。
- 不知道 Hook 顺序错乱的根本原因是状态槽位映射失效。
速记要点(可背诵)
- 状态挂在 Fiber,不挂在函数局部变量。
useState对应 Hook 链表中的一个节点。setState是入队 + 调度,不是直接改值。- 下次 render 按调用顺序处理队列,得到新 state。