文章

useState实现原理

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 引入自动批处理,setTimeoutPromise.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 标志,为 truedispatch 只入队不调度;事件/异步回调结束后统一 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 是函数还是值。

「其实你每天都在用」

  1. 表单输入绑定const [value, setValue] = useState('') + <input onChange={e => setValue(e.target.value)} />
  2. 开关/弹窗状态const [visible, setVisible] = useState(false),打开/关闭。
  3. 请求三态data/loading/error 三个 useState 或一个 {data, loading, error} 对象。
  4. 列表选中项const [selected, setSelected] = useState<Set<string>>(new Set()),复选框交互。
  5. 函数式更新解决闭包陷阱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 一半的秘密。

本文由作者按照 CC BY 4.0 进行授权