跳到主要内容

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 读到的 state
  • queue:待处理的更新队列
  • next:下一个 Hook 节点

所以多个 Hooks 并不是存在数组里,而是挂成一条链表。

三、首次渲染时 useState 做了什么

首次渲染通常叫 mount。

这时 React 会:

  1. 创建一个新的 Hook 节点。
  2. 把初始值放进 memoizedState
  3. 创建更新队列 queue
  4. 生成一个稳定的 dispatch 函数。
  5. 把这个 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 改掉”。

真实过程更接近:

  1. 创建一个 update 对象。
  2. 把它塞进该 Hook 的更新队列。
  3. 给当前更新分配优先级。
  4. 调度对应 Fiber 重新 render。

也就是说:

  • setState 负责“登记更新”
  • 真正“算出新 state”发生在下一轮 render

一个 update 可以是:

  • 直接值:setCount(1)
  • 函数式更新:setCount(prev => prev + 1)

函数式更新之所以重要,是因为它基于队列里的前一个结果继续算,更适合连续更新场景。

五、下一次 render 时怎么得到新 state

更新阶段通常叫 update。

React 重新执行组件时,会再次按顺序走到 useState,这时不是重新创建,而是:

  1. 取到旧 Hook 对应的新 Hook 节点。
  2. 读取它的更新队列。
  3. 把队列里的 action 按顺序应用到 baseState
  4. 计算出新的 memoizedState
  5. 返回 [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 链表,每个 useState Hook 节点里保存当前 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。