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.8 | useState、useEffect、useContext、useReducer、useMemo、useCallback、useRef、useLayoutEffect 等 | Hook 链表、Dispatcher、UpdateQueue |
| 18.0 | useId、useSyncExternalStore、useInsertionEffect、useTransition、useDeferredValue、startTransition | TransitionLanes、自动批处理 |
| 19.0 | use、useActionState、useOptimistic;useFormStatus(react-dom) | Actions、use 可条件调用、可挂起 |
| 章节 | 内容 | 版本 |
|---|---|---|
| §3–§8 | 链表、规则、state / effect / memo / context | 16.8+ |
| §9 | 并发相关 Hook | 18+ |
| §10 | Actions、use、乐观更新 | 19+ |
| §11 | Dispatcher | 16.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 最小实现(链表节点)
// 伪代码:每调一次 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
- 只在函数组件或自定义 Hook 顶层调用。
- 不在循环、条件、普通嵌套函数里调用(19 的
use例外,见 §10)。 - 每次渲染调用次数与顺序必须一致。
4.2 为何必须稳定顺序
React 没有「Hook 名」,靠第几次调用对齐链表槽位。条件分支里多调/少调 useState,后续槽位全错位,表现为状态串线。
19:use(promise|context) 不占固定槽位,故可条件调用。
5. useState 实现(React 16.8)
[state, setState] 背后:Dispatcher → Hook + queue → dispatchSetState 入队 → Lanes 调度 → 下次渲染 updateReducer 合并。
5.0 三个易混淆点
| 使用者以为 | 实际 |
|---|---|
setState 立刻改 state | 多数只入队 Update,合并发生在下次 updateState |
每次 useState 新建 Hook | 仅 mount 创建;update 按序复用节点 |
| 每次新的 setState 函数 | mount 时 bind fiber+queue,引用稳定 |
数据结构长什么样(两个 useState)
假设:
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 │ // 链尾
└─────────────────────┘5.1 入口
react 包 useState → resolveDispatcher() → mountState / updateState(react-reconciler)
初始化时,useState 调用的是 mountState,在更新时,useState 调用的是 updateState
5.2 mount 阶段
mountWorkInProgressHook()追加链表节点。- 初始化
memoizedState/baseState;函数形initialState仅执行一次。 - 创建 UpdateQueue(
pending环形链表、lanes、dispatch)。 dispatchSetState.bind(fiber, queue)作为 setState。
5.3 最小实现
// 伪代码:与 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。
所以同一行代码里:
setCount(count + 1)
console.log(count) // 还是旧值,正常要想连续改、别被闭包里的旧 count 坑到,用函数式更新:setCount(c => c + 1)。要在 state 变了之后再做事,用 useEffect 依赖 [count],或在回调里用自己算好的局部变量。
二、setState 的「合并」是什么?
「合并」常指两件事,别混在一起:
| 含义 | 简单说 |
|---|---|
| 少渲染几次(批处理) | 同一次点击里 setState 三次,React 18+ 往往只再画一遍界面 |
| 一次渲染里算清最终 state | 下次渲染时 updateReducer 把 queue.pending 环里排队的多个 action 按顺序算完,才写入 memoizedState |
函数式更新 setState(prev => …) 里的 prev,是上一个 action 算完后的结果,不是组件里那个旧的 state 变量。所以连写两次 setCount(c => c + 1),可以从 0 变到 2,而不是两次都从 0 加到 1。
三、Hook 链表 和 queue.pending 环,怎么区分?
这是两套结构,各管一件事:
Hook 链表(hook.next) | queue.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)
- 打包
create、deps、destroy(初值 undefined)为 Effect。 HookHasEffect:本轮要在 Commit 后跑。- update 时
areHookInputsEqual(deps)为真可去掉标记,整段跳过。 PassiveEffectflag:参与 Passive 队列(与useLayoutEffect的 Layout 子阶段区分)。
6.2 Effect 环形链表
挂在 updateQueue.lastEffect,环形便于 O(1) 插入。Commit 遍历:有 HookHasEffect 则先 destroy 再 create,写回新 destroy。
6.3 Commit 时机
| 子阶段 | useLayoutEffect | useEffect |
|---|---|---|
| Before Mutation | destroy | — |
| Mutation | DOM 更新 | DOM 更新 |
| Layout | create | — |
| Passive | — | destroy → create |
要同次提交内测布局/改 DOM → useLayoutEffect;否则可能闪屏。
flowchart TB
R[Render 登记] --> M[Mutation]
M --> L[Layout]
L --> P[Passive useEffect]6.4 最小实现
// 伪代码: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/destroy7. useMemo / useCallback(React 16.8)
memoizedState 存 **[缓存值或函数, deps]**。areHookInputsEqual 为真则跳过 nextCreate()。
| 点 | 说明 |
|---|---|
无 deps(undefined→null) | 每轮重算,等同未写依赖数组 |
[] | 仅 mount 算一次(Strict Mode 开发态可能双 mount) |
| useCallback | 等价于 useMemo(() => fn, deps),缓存引用 |
滥用增加比较开销;优先昂贵计算 + 配合 memo 子组件。
7.1 最小实现
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 共用路径)
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.57.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。
例:一个组件里同时使用 useState 和 useRef。
function SearchBox() {
const [keyword, setKeyword] = useState('')
const inputRef = useRef(null)
return <input ref={inputRef} value={keyword} onChange={e => setKeyword(e.target.value)} />
}对应到当前函数组件 Fiber 上,大致是:
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,因此不会触发渲染。
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 最小实现
// 伪代码:沿 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-Fiber 的 TransitionLanes 与批处理。
| Hook / API | 作用 | 实现要点 |
|---|---|---|
| useTransition / startTransition | 非紧急更新 | 落入 TransitionLanes;isPending;可打断 |
| useDeferredValue | 滞后显示重内容 | 滞后拷贝 + Transition 追上;19 可选 initialValue |
| useId | SSR/客户端一致 id | Fiber 树计数器,非 queue |
| useSyncExternalStore | 外部 store 并发安全 | subscribe + getSnapshot;Zustand/Redux 对接 |
| useInsertionEffect | CSS-in-JS 注入时机 | DOM 后、Layout 前 |
9.1 最小实现(节选)
// 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 | 过渡期间乐观 UI | Transition 内临时值,完成后对齐真实 state |
| use | 读 Promise / Context | 可条件调用;Promise 未就绪则 Suspense |
| useFormStatus(react-dom) | 父 form 的 pending 等 | 表单上下文,非 react 包 |
其它:ref 作 prop、异步 startTransition、RSC 稳定 — 见 07-RSC。
10.1 最小实现(节选)
// 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 最小实现
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-常见问题)
| 陷阱 | 原因 | 处理 |
|---|---|---|
| 闭包旧 state | effect / 定时器捕获旧渲染 | setState(p => …) 或补依赖 |
| 空依赖却用 props | effect 不随 props 更新 | exhaustive-deps |
| 条件里写 Hook | 槽位错位 | 提升到顶层;或 19 用 use 读资源 |
14. 源码阅读路径
| 顺序 | 文件 | 关注 |
|---|---|---|
| 1 | ReactHooks.js | 公开 API、resolveDispatcher |
| 2 | ReactFiberHooks.js | mountState、updateReducer、dispatchSetState |
| 3 | ReactFiberCommitWork.js | Effect mount/unmount、Passive |
| 4 | 本文 §5.5–§11.1 | 最小伪代码,建立整体调用关系后再读真源码 |
15. 总结
15.1 四条主线
| 主线 | 机制 | API |
|---|---|---|
| 状态 | 链表 + UpdateQueue + Lanes | useState、useReducer |
| 副作用 | Effect 环 + Commit 分阶段 | useEffect、useLayoutEffect |
| 记忆化 | [value, deps] | useMemo、useCallback |
| 跨树 | Provider 值 | useContext |
扩展:18 并发 / 外部 store;19 Actions / use。
15.2 设计取舍
链表 + 顺序、Dispatcher 分派、更新入队与 DOM/effect 解耦、Effect 推迟到 Commit — 支撑批处理、并发与可测试性。