Skip to content
目录

Hooks 实现机制

概念、流程、对照表为主;各章附 最小伪代码(教学用,非 React 源码)。完整实现见 packages/react* / ReactFiberHooks.js。写作约定:../../写作约定.md

1. Hooks 是什么

Hooks 是 React 16.8(2019-02)引入的机制,允许在函数组件中使用 state、副作用与上下文。此后每个大版本都会增补少量 Hook,并与 Lanes / Transition / Actions 等调度能力配合;排错时先确认「API 从哪一版开始」可避免把 18/19 行为当成 16.8 语义。


2. Hooks 版本一览

版本新增 / 强相关与实现的关系
16.8useStateuseEffectuseContextuseReduceruseMemouseCallbackuseRefuseLayoutEffectHook 链表、Dispatcher、UpdateQueue
18.0useIduseSyncExternalStoreuseInsertionEffectuseTransitionuseDeferredValuestartTransitionTransitionLanes、自动批处理
19.0useuseActionStateuseOptimisticuseFormStatus(react-dom)Actions、use 可条件调用、可挂起
章节内容版本
§3–§8链表、规则、state / effect / memo / context16.8+
§9并发相关 Hook18+
§10Actions、use、乐观更新19+
§11Dispatcher16.8+

未单独开章:useReducer(共用 updateReducer)、useRef / useLayoutEffect(状态在 memoizedState,无独立 queue)。


3. Hooks 的数据结构

3.1 Hook 对象(字段含义)

源码:ReactFiberHooks.js。每个 Hook 节点大致包含:

字段含义
memoizedState对外可读的状态(state 值、effect 对象、memo 缓存等)
baseState合并更新时的基准;并发下 rebase 用
baseQueue因优先级暂缓、待恢复的 Update
queue有状态 Hook 的更新队列(如 useState)
next链上下一个 Hook

3.2 Hooks 链表

同一 Fiber 上多次调用 Hook → 按调用顺序串成链表,头指针在 **fiber.memoizedState**。
例:先 useState、再 useState、再 useEffect → 三个节点依次 next 连接。

3.3 为何用链表

  • 运行时才知道 Hook 个数;链表易扩容。
  • 顺序即身份:第 N 次调用对应第 N 个节点。
  • 遍历匹配 mount/update,实现简单。

3.4 最小实现(链表节点)

js
// 伪代码:每调一次 Hook API 追加一个节点
function mountWorkInProgressHook() {
  const hook = { memoizedState: null, baseState: null, queue: null, next: null }
  if (workInProgressHook === null) {
    currentlyRenderingFiber.memoizedState = workInProgressHook = hook
  } else {
    workInProgressHook = workInProgressHook.next = hook
  }
  return hook
}

function updateWorkInProgressHook() {
  if (currentHook === null) {
    currentHook = currentlyRenderingFiber.alternate.memoizedState
  } else {
    currentHook = currentHook.next
  }
  workInProgressHook = currentHook
  return workInProgressHook
}

3.5 特别注意 hook 和 fiber的区别

fiber.memoizedState:函数组件的 Hook 链表头(第一个 Hook 对象)。

hook.memoizedState:该 Hook 的 槽位状态(state 值、Effect 描述、memo 缓存、ref 对象等)。

同名不同层:Fiber 管「有哪些 Hook、顺序如何」;Hook 管「这个 Hook 记住了什么」。读源码时看到 memoizedState 先问「当前变量是 Fiber 还是 Hook」。


4. Hooks 的调用规则

4.1 Rules of Hooks

  1. 只在函数组件或自定义 Hook 顶层调用。
  2. 不在循环、条件、普通嵌套函数里调用(19 的 use 例外,见 §10)。
  3. 每次渲染调用次数与顺序必须一致。

4.2 为何必须稳定顺序

React 没有「Hook 名」,靠第几次调用对齐链表槽位。条件分支里多调/少调 useState,后续槽位全错位,表现为状态串线。
19use(promise|context) 不占固定槽位,故可条件调用。


5. useState 实现(React 16.8)

[state, setState] 背后:DispatcherHook + queuedispatchSetState 入队Lanes 调度 → 下次渲染 updateReducer 合并

5.0 三个易混淆点

使用者以为实际
setState 立刻改 state多数只入队 Update,合并发生在下次 updateState
每次 useState 新建 Hook仅 mount 创建;update 按序复用节点
每次新的 setState 函数mount 时 bind fiber+queue,引用稳定

数据结构长什么样(两个 useState)

假设:

tsx
const AComp = () => {
  const [a, setA] = useState(0)   // 第 1 次调用
  const [b, setB] = useState('')  // 第 2 次调用
  return <>dsadsadsa</>
}

对应这一颗函数组件 Fiber(简化):

Fiber (AComp)
├── type: AComp                    // 组件函数本身
├── memoizedState ───────────────┐ // 注意:这是 Hook 链头,不是 state 值
├── child ──► Fragment Fiber     │
├── sibling / return ...

                    ┌─────────────────────┐
                    │ Hook #1 (第1次useState)│
memoizedState: 0      │  ← 槽位里的 a
queue: { pending, dispatch: setA }
                    │ next ───────────────┐
                    └─────────────────────┘

                    ┌─────────────────────┐
                    │ Hook #2 (第2次useState)│
memoizedState: ''     │  ← 槽位里的 b
queue: { pending, dispatch: setB }
next: null// 链尾
                    └─────────────────────┘

延伸阅读:jser.dev useState 内部分析

5.1 入口

reactuseStateresolveDispatcher()mountState / updateStatereact-reconciler

初始化时,useState 调用的是 mountState,在更新时,useState 调用的是 updateState

5.2 mount 阶段

  1. mountWorkInProgressHook() 追加链表节点。
  2. 初始化 memoizedState / baseState;函数形 initialState 仅执行一次。
  3. 创建 UpdateQueuepending 环形链表、lanesdispatch)。
  4. dispatchSetState.bind(fiber, queue) 作为 setState。

5.3 最小实现

js
// 伪代码:与 useReducer 共用 reducer 路径
function mountState(initialState) {
  const hook = mountWorkInProgressHook()
  const state =
    typeof initialState === 'function' ? initialState() : initialState
  hook.memoizedState = hook.baseState = state
  hook.queue = { pending: null, lanes: NoLanes, dispatch: null }
  const dispatch = (hook.queue.dispatch = dispatchSetState.bind(
    null,
    currentlyRenderingFiber,
    hook.queue,
  ))
  return [hook.memoizedState, dispatch]
}

function updateReducer(reducer = basicStateReducer) {
  const hook = updateWorkInProgressHook()
  const queue = hook.queue
  let newState = hook.baseState
  // 简化:遍历 pending 环形链表上的 update,依次 reducer(state, action)
  let update = queue.pending
  if (update !== null) {
    do {
      newState = reducer(newState, update.action)
      update = update.next
    } while (update !== queue.pending)
    queue.pending = null
  }
  hook.memoizedState = hook.baseState = newState
  return [hook.memoizedState, queue.dispatch]
}

function dispatchSetState(fiber, queue, action) {
  const lane = requestUpdateLane(fiber)
  const update = { lane, action, next: null }
  enqueueUpdate(queue, update) // 挂到 pending 环
  scheduleUpdateOnFiber(fiber, lane)
}

function useState(initialState) {
  const dispatcher = resolveDispatcher()
  return dispatcher.useState(initialState)
}
// HooksDispatcherOnMount.useState = mountState
// HooksDispatcherOnUpdate.useState = () => updateReducer(basicStateReducer)

常见疑问

一、setState 之后,为什么拿不到最新值?

useState 返回的 state,是这一轮渲染已经从 Hook 里读出来的数。setState 多半只是把一次修改登记进队列,并安排「再渲染一次」;真正改 hook.memoizedState 发生在下一次渲染里的 updateReducer

所以同一行代码里:

js
setCount(count + 1)
console.log(count) // 还是旧值,正常

要想连续改、别被闭包里的旧 count 坑到,用函数式更新:setCount(c => c + 1)。要在 state 变了之后再做事,用 useEffect 依赖 [count],或在回调里用自己算好的局部变量。

二、setState 的「合并」是什么?

「合并」常指两件事,别混在一起:

含义简单说
少渲染几次(批处理)同一次点击里 setState 三次,React 18+ 往往只再画一遍界面
一次渲染里算清最终 state下次渲染时 updateReducerqueue.pending 环里排队的多个 action 按顺序算完,才写入 memoizedState

函数式更新 setState(prev => …) 里的 prev,是上一个 action 算完后的结果,不是组件里那个旧的 state 变量。所以连写两次 setCount(c => c + 1),可以从 0 变到 2,而不是两次都从 0 加到 1。

三、Hook 链表 和 queue.pending 环,怎么区分?

这是两套结构,各管一件事:

Hook 链表(hook.nextqueue.pending
管什么这个组件里第 1、2、3… 次调用的 Hook 各是谁某一个 useState 还没算进去的修改
头在哪fiber.memoizedState该 Hook 的 hook.queue.pending
打个比方一排编号固定的抽屉某一个抽屉上的「待办条」
  • Hook 链表:横向串起「第几次 useState / useEffect」;顺序错了,状态会串线(§4.2)。
  • pending 环:纵向只服务这一个 state Hook;每次 setState 往环里挂一条 Update,下次 updateReducer 遍历环、算完清空。

memoizedState:在 Fiber 上是 Hook 链头,在 Hook 上是这个槽当前的 state 值


6. useEffect 实现(React 16.8)

Render 只登记;执行在 Commit 的 Passive 阶段(异步)。状态在 Hook 链表;副作用在 **fiber.updateQueue 的 Effect 环形链表**。

6.1 登记(mount / update)

  • 打包 createdepsdestroy(初值 undefined)为 Effect
  • HookHasEffect:本轮要在 Commit 后跑。
  • update 时 areHookInputsEqual(deps) 为真可去掉标记,整段跳过。
  • PassiveEffect flag:参与 Passive 队列(与 useLayoutEffect 的 Layout 子阶段区分)。

6.2 Effect 环形链表

挂在 updateQueue.lastEffect环形便于 O(1) 插入。Commit 遍历:有 HookHasEffect 则先 destroycreate,写回新 destroy。

6.3 Commit 时机

子阶段useLayoutEffectuseEffect
Before Mutationdestroy
MutationDOM 更新DOM 更新
Layoutcreate
Passivedestroy → create

要同次提交内测布局/改 DOM → useLayoutEffect;否则可能闪屏。

mermaid
flowchart TB
  R[Render 登记] --> M[Mutation]
  M --> L[Layout]
  L --> P[Passive useEffect]

6.4 最小实现

js
// 伪代码:Render 只入队;Commit Passive 阶段再执行
function mountEffect(create, deps) {
  const hook = mountWorkInProgressHook()
  const effect = { create, destroy: undefined, deps, tag: HookHasEffect | PassiveEffect }
  hook.memoizedState = effect
  pushEffect(fiber.updateQueue, effect) // 挂到 Effect 环形链表
}

function updateEffect(create, deps) {
  const hook = updateWorkInProgressHook()
  const prev = hook.memoizedState
  if (areHookInputsEqual(prev.deps, deps)) {
    prev.tag &= ~HookHasEffect // 依赖未变,Commit 跳过
  } else {
    prev.deps = deps
    prev.create = create
    prev.tag |= HookHasEffect
  }
}

function commitPassiveEffects(root) {
  for (const effect of passiveEffectList) {
    if (!(effect.tag & HookHasEffect)) continue
    if (effect.destroy) effect.destroy()
    effect.destroy = effect.create() // create 返回 cleanup
  }
}

function useEffect(create, deps) {
  return resolveDispatcher().useEffect(create, deps)
}

// useLayoutEffect:同上,但 tag 为 LayoutEffect,在 commitLayoutEffects 同步执行 create/destroy

7. useMemo / useCallback(React 16.8)

memoizedState**[缓存值或函数, deps]**areHookInputsEqual 为真则跳过 nextCreate()

说明
无 deps(undefinednull每轮重算,等同未写依赖数组
[]仅 mount 算一次(Strict Mode 开发态可能双 mount)
useCallback等价于 useMemo(() => fn, deps),缓存引用

滥用增加比较开销;优先昂贵计算 + 配合 memo 子组件。

7.1 最小实现

js
function mountMemo(nextCreate, deps) {
  const hook = mountWorkInProgressHook()
  const value = nextCreate()
  hook.memoizedState = [value, deps]
  return value
}

function updateMemo(nextCreate, deps) {
  const hook = updateWorkInProgressHook()
  const [prevValue, prevDeps] = hook.memoizedState
  if (areHookInputsEqual(prevDeps, deps)) return prevValue
  const value = nextCreate()
  hook.memoizedState = [value, deps]
  return value
}

function useCallback(callback, deps) {
  return useMemo(() => callback, deps)
}

function useMemo(factory, deps) {
  return resolveDispatcher().useMemo(factory, deps)
}

7.2 useReducer(与 useState 共用路径)

js
function useReducer(reducer, initialArg, init) {
  const initState = init ? init(initialArg) : initialArg
  const [state, dispatch] = useState(initState)
  const boundDispatch = (action) => dispatch((s) => reducer(s, action))
  return [state, boundDispatch]
}
// 真实源码:mountReducer / updateReducer(reducer),queue 存 action,合并逻辑同 §5.5

7.3 useRef

useRef(initialValue) 返回一个稳定对象:{ current: initialValue }。这个对象存在当前 Hook 的 memoizedState 里,后续渲染只把同一个对象取出来。

说明
mount创建 { current },放到 hook.memoizedState
update直接返回上次的 hook.memoizedState
ref.current只是改对象属性,不入队、不调度、不触发渲染
和 state 区别useState 改值会走 queue + schedule;useRef 没有更新队列

所以 ref 适合存「需要跨渲染保存,但不影响 UI 的值」,例如 DOM 节点、定时器 id、上一次值、可变实例。若修改后希望界面变化,应使用 useState

例:一个组件里同时使用 useStateuseRef

js
function SearchBox() {
  const [keyword, setKeyword] = useState('')
  const inputRef = useRef(null)
  return <input ref={inputRef} value={keyword} onChange={e => setKeyword(e.target.value)} />
}

对应到当前函数组件 Fiber 上,大致是:

txt
Fiber (SearchBox)
├── type: SearchBox
├── memoizedState ───────────────┐ // Hook 链头
├── child ──► input Fiber        │
├── sibling / return ...         │

                    ┌─────────────────────┐
                    │ Hook #1 (useState)  │
                    │ memoizedState: ''    │  ← keyword
                    │ queue: { pending, dispatch: setKeyword }
                    │ next ───────────────┐
                    └─────────────────────┘

                    ┌─────────────────────┐
                    │ Hook #2 (useRef)    │
                    │ memoizedState:      │  ← inputRef 对象本身
                    │   { current: input }│
                    │ queue: null         │  // 无更新队列
                    │ next: null          │
                    └─────────────────────┘

inputRef.current = xxx 只是在改第二个 Hook 里那个对象的属性;没有 dispatch,也不会进入 queue.pending,因此不会触发渲染。

js
function mountRef(initialValue) {
  const hook = mountWorkInProgressHook()
  hook.memoizedState = { current: initialValue }
  return hook.memoizedState
}

function updateRef() {
  const hook = updateWorkInProgressHook()
  return hook.memoizedState // 同一对象,不触发更新
}

8. useContext(React 16.8)

readContext → 读当前遍历到的 Provider **_currentValue**。不把 value 存入 Hook queue,无 setContext。

Provider value 变 → propagateContextChange 标记消费者 lanes → 消费组件重渲染(可突破 memo 浅比较)。
粒度宜细:拆 Context 或 composition 减订阅面。

8.1 最小实现

js
// 伪代码:沿 Fiber 树向上找 Provider,读 _currentValue
function readContext(Context) {
  const providerFiber = findNearestProvider(currentlyRenderingFiber, Context)
  return providerFiber.memoizedProps.value // 简化:真实为 dependency 追踪 + lanes
}

function useContext(Context) {
  const value = readContext(Context)
  // mount/update 仍会占一个 Hook 槽(用于 DEV 与订阅列表),但不存 value 到 queue
  mountWorkInProgressHook() // 或 updateWorkInProgressHook,视实现版本
  return value
}

function propagateContextChange(providerFiber, context, newValue) {
  // 遍历订阅了该 context 的子 Fiber,标记 lanes,scheduleUpdate
}

9. React 18 新增 Hooks

依赖 02-FiberTransitionLanes 与批处理。

Hook / API作用实现要点
useTransition / startTransition非紧急更新落入 TransitionLanes;isPending;可打断
useDeferredValue滞后显示重内容滞后拷贝 + Transition 追上;19 可选 initialValue
useIdSSR/客户端一致 idFiber 树计数器,非 queue
useSyncExternalStore外部 store 并发安全subscribe + getSnapshot;Zustand/Redux 对接
useInsertionEffectCSS-in-JS 注入时机DOM 后、Layout 前

9.1 最小实现(节选)

js
// useTransition:把回调里的 setState 标为 TransitionLane
function useTransition() {
  const [isPending, setPending] = useState(false)
  const startTransition = (callback) => {
    const prev = ReactCurrentBatchConfig.transition
    ReactCurrentBatchConfig.transition = {} // 开启 transition 上下文
    setPending(true)
    try {
      callback()
    } finally {
      ReactCurrentBatchConfig.transition = prev
      setPending(false)
    }
  }
  return [isPending, startTransition]
}

// useDeferredValue:存上一帧值 + 用 transition 追赶最新 value
function useDeferredValue(value) {
  const [deferred, setDeferred] = useState(value)
  useEffect(() => {
    startTransition(() => setDeferred(value))
  }, [value])
  return deferred
}

// useSyncExternalStore:getSnapshot 必须与 subscribe 对齐,避免 tearing
function useSyncExternalStore(subscribe, getSnapshot, getServerSnapshot) {
  const value = getSnapshot()
  useEffect(() => {
    return subscribe(() => forceRerender()) // 外部 store 变 → 调度更新
  }, [subscribe])
  return isServer ? getServerSnapshot() : value
}

// useId:Fiber 树路径上递增计数器,SSR/CSR 同规则生成
function useId() {
  const hook = updateWorkInProgressHook()
  if (hook.memoizedState == null) {
    hook.memoizedState = makeId(currentlyRenderingFiber)
  }
  return hook.memoizedState
}

10. React 19 新增 Hooks

Hook作用实现要点
useActionState表单 / Server Action 状态机基于 reducer + Action;isPending;曾名 useFormState
useOptimistic过渡期间乐观 UITransition 内临时值,完成后对齐真实 state
use读 Promise / Context可条件调用;Promise 未就绪则 Suspense
useFormStatus(react-dom)父 form 的 pending 等表单上下文,非 react 包

其它:ref 作 prop、异步 startTransition、RSC 稳定 — 见 07-RSC

10.1 最小实现(节选)

js
// use(promise):未 resolve 则抛给 Suspense;已 resolve 读缓存
function use(usable) {
  if (usable && typeof usable.then === 'function') {
    const status = readThenableState(usable) // pending | fulfilled | rejected
    if (status.pending) throw usable // Suspense 边界捕获
    if (status.rejected) throw status.reason
    return status.value
  }
  return readContext(usable) // Context 也可 use()
}

// useActionState:reducer + 包装 async action,暴露 pending
function useActionState(action, initialState) {
  const [state, dispatch] = useReducer(reducer, initialState)
  const [isPending, startTransition] = useTransition()
  const boundAction = async (payload) => {
    startTransition(async () => {
      const next = await action(state, payload)
      dispatch({ type: 'set', payload: next })
    })
  }
  return [state, boundAction, isPending]
}

// useOptimistic:真实 state + 过渡期间的乐观副本
function useOptimistic(passthrough, reducer) {
  const [optimistic, setOptimistic] = useState(passthrough)
  useEffect(() => setOptimistic(passthrough), [passthrough])
  const update = (action) => {
    startTransition(() => {
      setOptimistic((current) => reducer(current, action))
    })
  }
  return [optimistic, update]
}

11. Hooks 的 Dispatcher

ReactCurrentDispatcher.current 指向当前允许使用的 Hook 实现表(mount / update / rerender / 非法调用桩)。

Dispatcher场景
OnMount首次渲染,建链表
OnUpdate常规更新
OnRerender渲染中再次执行
Invalid组件外调用 → 报错

协调器根据 current.memoizedState 是否为空切换;普通函数内无有效 Fiber,不能调 Hook。

11.1 最小实现

js
const HooksDispatcherOnMount = {
  useState: mountState,
  useEffect: mountEffect,
  useMemo: mountMemo,
  useContext: readContext, // + 登记 Hook 槽
  // ...
}
const HooksDispatcherOnUpdate = {
  useState: () => updateReducer(basicStateReducer),
  useEffect: updateEffect,
  useMemo: updateMemo,
  // ...
}

function resolveDispatcher() {
  const dispatcher = ReactCurrentDispatcher.current
  if (dispatcher === null) throw new Error('Invalid hook call')
  return dispatcher
}

function renderWithHooks(Component, props) {
  currentlyRenderingFiber = workInProgress
  ReactCurrentDispatcher.current =
    workInProgress.alternate === null
      ? HooksDispatcherOnMount
      : HooksDispatcherOnUpdate
  const children = Component(props)
  ReactCurrentDispatcher.current = null
  return children
}

12. 性能与用法(开发侧)

主题建议
批处理18+ 多场景自动批;同事件多次 setState 常一次渲染
useMemo / deps写全依赖;昂贵计算才 memo
useCallback子组件 memo 时再考虑稳定引用
useTransition大更新标非紧急,保输入流畅

13. 常见陷阱(详见 09-常见问题

陷阱原因处理
闭包旧 stateeffect / 定时器捕获旧渲染setState(p => …) 或补依赖
空依赖却用 propseffect 不随 props 更新exhaustive-deps
条件里写 Hook槽位错位提升到顶层;或 19 用 use 读资源

14. 源码阅读路径

顺序文件关注
1ReactHooks.js公开 API、resolveDispatcher
2ReactFiberHooks.jsmountStateupdateReducerdispatchSetState
3ReactFiberCommitWork.jsEffect mount/unmount、Passive
4本文 §5.5–§11.1最小伪代码,建立整体调用关系后再读真源码

15. 总结

15.1 四条主线

主线机制API
状态链表 + UpdateQueue + LanesuseState、useReducer
副作用Effect 环 + Commit 分阶段useEffect、useLayoutEffect
记忆化[value, deps]useMemo、useCallback
跨树Provider 值useContext

扩展:18 并发 / 外部 store;19 Actions / use

15.2 设计取舍

链表 + 顺序、Dispatcher 分派、更新入队与 DOM/effect 解耦、Effect 推迟到 Commit — 支撑批处理、并发与可测试性。

参考

MIT License.