但行好事 莫问前程
前言
出于以下动机:
- 简化组件:用函数替代类,避免
this和生命周期混乱。 - 逻辑复用:通过自定义 Hook 抽离重复逻辑(如数据请求、监听事件)。
- 功能聚焦:每个 Hook 专注一个功能(如
useState管状态,useEffect管副作用)。
React 16.8 新增特性 —— Hook,让函数式组件也能使用 状态state 和 其他 react 特性。
本文代码基于v18.3.1,内容精确到每一个变量,每一行代码,一起从源码层面学习 hooks 的工作机制。
简介
在学习源码之前,你应该知道以下内容:
- React 使用 双缓冲机制(Double Buffering) 实现 Fiber 树的更新
- Hooks 以 链表形式 存储在 Fiber 的
memoizedState属性中,每个 Hook 对应链表的一个节点
type Hook = {
memoizedState: any, // 当前状态(如 useState 的值、useEffect 的依赖、useRef的ref对象...)
baseState: any, // 基础状态(用于更新计算)
baseQueue: Update<any, any> | null, // 待处理的更新队列
queue: any,// 待更新队列(如 setState 的批量更新)、用于更新的dispatch函数、更新优先级Lanes等信息
next: Hook | null, // 指向下一个 Hook 节点,用于构成链表
};
type fiber = {
memoizedState: Hook | null, // Hooks 链表的头节点
// ...其他 Fiber 字段(如 stateNode、alternate)
}
然后认识一些重要的变量:
current: 指向 当前已渲染的 Fiber 树(旧,对应屏幕上显示的内容)。workInProgress: 指向 正在构建的新 Fiber 树(新,内存中构建的更新版本)。currentHook: 已渲染的 Fiber 树 中,正在处理的 Hook 节点。workInProgressHook: 新的 Fiber 树 中,正在构建或更新的 Hook 节点。currentlyRenderingFiber: 当前正在执行的函数组件对应的 Fiber 节点(即workInProgressFiber)。ReactCurrentDispatcher: 调度器对象,根据当前渲染阶段(Mount or Update)分配不同的调度器。
函数组件的更新和渲染,会触发 updateFunctionComponent 函数,调用 renderWithHooks 函数 -> 执行 Component(props) 组件函数。
我们以 renderWithHooks 为入口,分析 Hook 是如何运行的。
renderWithHooks
react/packages/react-reconciler/src/ReactFiberHooks.new.js
抛开_DEV_相关的调试代码和一些注释,精简 renderWithHooks 函数:
export function renderWithHooks<Props, SecondArg>(
current: Fiber | null,
workInProgress: Fiber,
Component: (p: Props, arg: SecondArg) => any,
props: Props,
secondArg: SecondArg,
nextRenderLanes: Lanes
): any {
renderLanes = nextRenderLanes;
// 1. 设置当前正在渲染的 Fiber 节点
currentlyRenderingFiber = workInProgress;
// 2. 重置 Hooks 链表指针(初次渲染或更新)
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
// 3. 区分 Mount 和 Update 阶段,选择 Hooks 实现
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
// 4. 执行组件函数(内部会按顺序调用 Hooks)
let children = Component(props, secondArg);
// 5. 重置全局变量,准备下一轮渲染
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
return children;
}
解读
分块解读上述代码:
- 设置当前正在渲染的 Fiber 节点
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
renderLanes = nextRenderLanes;
设置渲染的优先级,在 React16 的早期版本中,官方采用 expirationTime 模型来调度渲染的优先级,在 React17+ 等版本中被 Lane 模型所替代。
expirationTime 模型仅通过时间设置优先级,存在灵活性差、优先级冲突(如用户的输入可能无法得到优先处理)、优先级单一等问题。
currentlyRenderingFiber = workInProgress;
如前文所说,通过 currentlyRenderingFiber 变量,可以正确从全局获取到 当前正在执行的函数组件对应的 Fiber 节点(即 workInProgress Fiber)。
- 重置 workInProgress fiber 状态
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
置空 workInProgress 的 Hooks链表、更新队列、渲染优先级,确保每次渲染都是纯净的。
后续 React 会根据函数组件的渲染阶段:
- 搭建一个新 hooks 链表
- 或者 复用
current.memoizedState即旧fiber Hooks 链表
赋值给 currentlyRenderingFiber.memoizedState 即 workInProgressHook。
- 区分 Mount 和 Update 阶段,选择 Hooks 实现方式
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
根据 current 和 memoizedState,判断函数组件状态:
- 首次渲染Mount,
ReactCurrentDispatcher.current = HooksDispatcherOnMount - 渲染更新Update,
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate
设置派发器 ReactCurrentDispatcher,确定如何执行组件中的 hooks。
- 执行组件函数(按顺序调用 Hooks)
let children = Component(props, secondArg);
Component(props, secondArg); 执行函数组件,函数中的 hooks 将被依次执行(useState、useEffect ...)。
- 重置全局变量,准备下一轮渲染
ReactCurrentDispatcher.current = ContextOnlyDispatcher;
renderLanes = NoLanes;
currentlyRenderingFiber = (null: any);
currentHook = null;
workInProgressHook = null;
ReactCurrentDispatcher.current = ContextOnlyDispatcher; 设置默认的派发器,避免 hooks 在非React函数中被调用。
最后,将相关变量重新置空。
小结
renderWithHooks 作为执行函数组件的核心,主要作用为:
- 设置当前正在渲染的 Fiber 节点
- 重置
workInProgressfiber 状态 - 区分 Mount 和 Update 阶段,选择 Hooks 实现方式
- 执行组件函数(按顺序调用 Hooks)
- 重置全局变量,准备下一轮渲染
接下来,我们继续探索 Hooks 的实现方式。
ReactCurrentDispatcher
上文说到 renderWithHooks 函数,根据函数组件的渲染状态,设置不同的派发器。
// 函数组件为 首次渲染Mount 阶段
const HooksDispatcherOnMount: Dispatcher = {
readContext,
useCallback: mountCallback,
useContext: readContext,
useEffect: mountEffect,
useImperativeHandle: mountImperativeHandle,
useLayoutEffect: mountLayoutEffect,
useInsertionEffect: mountInsertionEffect,
useMemo: mountMemo,
useReducer: mountReducer,
useRef: mountRef,
useState: mountState,
useDebugValue: mountDebugValue,
useDeferredValue: mountDeferredValue,
useTransition: mountTransition,
useMutableSource: mountMutableSource,
useSyncExternalStore: mountSyncExternalStore,
useId: mountId,
unstable_isNewReconciler: enableNewReconciler,
};
// 函数组件为 渲染更新Update 阶段
const HooksDispatcherOnUpdate: Dispatcher = {
readContext,
useCallback: updateCallback,
useContext: readContext,
useEffect: updateEffect,
useImperativeHandle: updateImperativeHandle,
useInsertionEffect: updateInsertionEffect,
useLayoutEffect: updateLayoutEffect,
useMemo: updateMemo,
useReducer: updateReducer,
useRef: updateRef,
useState: updateState,
useDebugValue: updateDebugValue,
useDeferredValue: updateDeferredValue,
useTransition: updateTransition,
useMutableSource: updateMutableSource,
useSyncExternalStore: updateSyncExternalStore,
useId: updateId,
unstable_isNewReconciler: enableNewReconciler,
};
可以看到在不同阶段,Hooks 的实现方式(函数)不同。
例如 useState:
- Mount 阶段 ——
useState: mountState - Update 阶段 ——
useState: updateState
下文以 useState 为例,看看 hooks 在不同阶段是如何执行的。
useState
mountState
function mountState(initialState) {
// 1. 挂载并获取最新的 Hooks 链表节点
const hook = mountWorkInProgressHook();
// 2. 对 hook 初始化赋值
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 待更新队列
lanes: NoLanes, // 更新队列的优先级
dispatch: null, // 负责更新state的方法
lastRenderedReducer: basicStateReducer, // 最后一次渲染的reducer
lastRenderedState: (initialState: any), // 最后一次渲染的state值
};
hook.queue = queue;
// 3. 确定触发函数更新的方法
const dispatch = (queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue))
return [hook.memoizedState, dispatch];
}
- 挂载并获取最新的 Hooks 链表节点
const hook = mountWorkInProgressHook();
处于 Mount阶段 的函数组件,每一次 Hooks 的执行,比如 useState、useEffect、useRef 都会调用 mountWorkInProgressHook 函数,创建新 hook 节点并挂载,以获取最新的 Hooks 链表节点。
- 对
hook初始化赋值
if (typeof initialState === 'function') {
initialState = initialState();
}
hook.memoizedState = hook.baseState = initialState;
const queue = {
pending: null, // 待更新队列
lanes: NoLanes, // 更新队列的优先级
dispatch: null, // 负责更新state的方法
lastRenderedReducer: basicStateReducer, // 最后一次渲染的reducer
lastRenderedState: (initialState: any), // 最后一次渲染的state值
};
hook.queue = queue;
useState(initialState)
如传递函数作为参数,initialState 被视为初始化函数,调用取值。
hook.memoizedState = hook.baseState = initialState;
将 initialState 赋值给 hook 节点的 memoizedState当前状态 和 baseState基础状态,作为初始状态。
hook.queue = queue;
新建空的队列,作为 hook 的更新队列。
- 确定触发函数更新的方法
queue.dispatch = dispatchSetState.bind(null, currentlyRenderingFiber, queue)
dispatchSetState.bind 绑定了 目标fibercurrentlyRenderingFiber 和 更新队列queue,但没有指定 action。
通过调用 setState,触发函数组件的状态更新 dispatchSetState 。
return [hook.memoizedState, dispatch];
返回 初始状态hook.memoizedState 和 更新状态的函数dispatch 即setXXX(nextState)。
mountWorkInProgressHook
function mountWorkInProgressHook(): Hook {
const hook: Hook = {
memoizedState: null,
baseState: null,
baseQueue: null,
queue: null,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = hook;
} else {
workInProgressHook = workInProgressHook.next = hook;
}
return workInProgressHook;
}
mountWorkInProgressHook 函数:
- 每次调用新建一个
hook节点,const hook: Hook = { //... } - 首次执行 Hooks,即 当前正在构建Fiber的Hooks链表 为空,
workInProgressHook === nullworkInProgressHook = hook;- 将
hook作为链表头节点
- 将
currentlyRenderingFiber.memoizedState = workInProgressHook- 将
workInProgressHook作为workInProgress的链表 - (前文说过
currentlyRenderingFiber指向最新的Fiber,fiber.memoizedState属性用于存储 Hooks 链表的头节点)
- 将
- 非首次执行 Hooks
workInProgressHook.next = hook;- 将
hook挂载到链表末尾
- 将
workInProgressHook = workInProgressHook.next- 将
workInProgressHook指向最新的hook
- 将
- 返回 新Fiber 的 Hooks 链表,
return workInProgressHook;
updateState
function updateState(initialState) {
return updateReducer(basicStateReducer, (initialState: any));
}
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
useState 在 Update阶段,本质是调用提前设置好 reducer 的 updateReducer 函数(reducer = basicStateReducer)。
updateReducer
抛开_DEV_相关的调试代码和一些注释,精简 updateReducer 函数:
function updateReducer(reducer, initialArg, init) {
// 1. 拷贝 current hook 到 workInProcess hook,并返回最新的 Hooks 链表节点
const hook = updateWorkInProgressHook();
// 2. 读取 hook.queue 中待执行的更新队列 pending,合并到 current.baseQueue 队尾
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const pendingQueue = queue.pending;
const current = currentHook;
let baseQueue = current.baseQueue;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
// 构成环装链表
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
// 3. 遍历 baseQueue 待更新队列,计算出最新的 state
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
//
// const updateLane = removeLanes(update.lane, OffscreenLane);
// const shouldSkipUpdate = ...
// if (shouldSkipUpdate) {
// ...
// 标记被跳过的更新
// markSkippedUpdateLanes(updateLane);
// }
// 随着遍历 不断更新 newState
if (update.hasEagerState) {
newState = update.eagerState;
} else {
const action = update.action;
newState = reducer(newState, action); // 得到最新的 state 值
}
update = update.next;
} while (update !== null && update !== first);
// 4. 判断是否存在未完成的更新
if (newBaseQueueLast === null) {
// 仅在全部更新完成时,更新 `newBaseState`
newBaseState = newState;
} else {
// 构建环形链表,供恢复时使用
newBaseQueueLast.next = newBaseQueueFirst;
}
// 当 state 值未变化时,跳过渲染
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
// 5. 更新 workInProgress Hook 中对应的值
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
const dispatch = queue.dispatch;
return [hook.memoizedState, dispatch];
}
Update阶段,useState/useReducer 要做的事就很多了,但我们可以简单理解为:
- 将
currentHook中对应的 hook 节点,克隆到workInProgressHook中,即 复用旧 Fiber 的 Hooks 链表 - 读取更新队列
queue,合并执行所有更新(dispatchAction) - 计算出最新状态
newState(期间可能跳过、中断、恢复),更新 hook 的状态
看看代码中的具体实现:
- 拷贝
current hook到workInProcess hook,并返回最新的 Hooks 链表节点
const hook = updateWorkInProgressHook();
处于 Update阶段 的函数组件,每一次 Hooks 的执行,比如 useState、useEffect、useRef 都会调用 updateWorkInProgressHook 函数,将 currentHook 链表 clone给 WorkInProgressHook 链表,并获取最新的 Hooks 链表节点。
hook(current -> workInProcess)
- 读取
hook.queue中待执行的更新队列pending,合并到current.baseQueue队尾
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
const pendingQueue = queue.pending;
const current = currentHook;
let baseQueue = current.baseQueue;
if (pendingQueue !== null) {
if (baseQueue !== null) {
const baseFirst = baseQueue.next;
const pendingFirst = pendingQueue.next;
// 构成环装链表
baseQueue.next = pendingFirst;
pendingQueue.next = baseFirst;
}
current.baseQueue = baseQueue = pendingQueue;
queue.pending = null;
}
pendingQueue = queue.pending;,读取了待执行的更新队列
pendingFirst = pendingQueue.next;,提供了 pendingQueue 的头节点
baseQueue.next = pendingFirst;,将 pendingQueue 合并到 baseQueue
在 React 中,渲染可能会被高优先级任务中断(如用户交互),此时 baseQueue 还未全部执行,所以使用合并链表的方式,而不是直接覆盖队列。
- 遍历
baseQueue待更新队列,计算出最新的state
if (baseQueue !== null) {
const first = baseQueue.next;
let newState = current.baseState;
let newBaseState = null;
let newBaseQueueFirst = null;
let newBaseQueueLast = null;
let update = first;
do {
//
// const updateLane = removeLanes(update.lane, OffscreenLane);
// const shouldSkipUpdate = ...
// if (shouldSkipUpdate) {
// ...
// 标记被跳过的更新
// markSkippedUpdateLanes(updateLane);
// }
// 随着遍历 不断更新 newState
if (update.hasEagerState) {
newState = update.eagerState;
} else {
const action = update.action;
newState = reducer(newState, action); // 得到最新的 state 值
}
update = update.next;
} while (update !== null && update !== first);
do { ... } while ( ... )
例如代码中多次调用 setCount
setCount(count=> count + 1);
setCount(count=> count + 1);
setCount(count=> count + 1);
每次 setCount 产生的 Update 都会被放入 pendingQueue 从而被合并到 baseQueue,从而计算出最终的状态 newState。
newState = reducer(newState, action);
调用reducer -> action,根据上一次 update 产生的 newState,并更新状态 newState =。
update.action 就是 setState(nextState) 中的 nextState,为一个新值或更新函数。
function basicStateReducer(state, action) {
return typeof action === 'function' ? action(state) : action;
}
调用流程为 dispatch(fiber, queue)(nextState) -> dispatchAction(fiber, queue, action)
if (update.hasEagerState)
在 dispatchAction(dispatchSetState) 中,会对比新旧状态 is(eagerState, currentState) 是否相同,从而决定是否执行更新
- 判断是否存在未完成的更新
if (newBaseQueueLast === null) {
// 仅在全部更新完成时,更新 `newBaseState`
newBaseState = newState;
} else {
// 构建环形链表,供恢复时使用
newBaseQueueLast.next = newBaseQueueFirst;
}
上文说过,渲染可能会被高优先级任务中断(如用户交互)。
newBaseQueueLast !== null,即更新队列还有任务时,只会更新 当前状态hook.memoizedState,不更新 基础状态hook.baseState。
- 更新
workInProgress Hook中对应的值
// 当 state 值未变化时,跳过渲染
if (!is(newState, hook.memoizedState)) {
markWorkInProgressReceivedUpdate();
}
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
最后更新 workInProgressHook 对应的状态。
queue.lastRenderedState = newState; 记录最后的状态值,方便下次更新时对比。
updateWorkInProgressHook
function updateWorkInProgressHook(): Hook {
// 移动 currentHook 指针
let nextCurrentHook: null | Hook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
if (current !== null) {
nextCurrentHook = current.memoizedState;
} else {
nextCurrentHook = null;
}
} else {
nextCurrentHook = currentHook.next;
}
// 移动 workInProgressHook 指针
let nextWorkInProgressHook: null | Hook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
workInProgressHook = nextWorkInProgressHook;
nextWorkInProgressHook = workInProgressHook.next;
currentHook = nextCurrentHook;
} else {
currentHook = nextCurrentHook;
const newHook: Hook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = workInProgressHook = newHook;
} else {
workInProgressHook = workInProgressHook.next = newHook;
}
}
return workInProgressHook;
}
函数看似较长,但目的只有一个 —— 将旧节点的 hook 拷贝出来,组成 workInProgressHook 链表。
- 每次 clone 一个
current hook节点,const newHook: Hook = { //... } - 首次执行 Hooks,即 当前正在构建Fiber的Hooks链表 为空,
workInProgressHook === nullworkInProgressHook = newHook;- 将
newHook作为链表头节点
- 将
currentlyRenderingFiber.memoizedState = workInProgressHook- 将
workInProgressHook作为workInProgress的链表 - (前文说过
currentlyRenderingFiber指向最新的Fiber,fiber.memoizedState属性用于存储 Hooks 链表的头节点)
- 将
- 非首次执行 Hooks
workInProgressHook.next = newHook;- 将
newHook挂载到链表末尾
- 将
workInProgressHook = workInProgressHook.next- 将
workInProgressHook指向最新的hook
- 将
- 返回 新Fiber 的 Hooks 链表,
return workInProgressHook;
总结
简单回顾流程:
- 渲染入口:
renderWithHooks设置currentlyRenderingFiber和ReactCurrentDispatcher。
- Hooks 调用:
useState、useEffect等从ReactCurrentDispatcher获取当前实现。
- 状态管理:
- Hooks 通过
currentHook和workInProgressHook操作新旧 Fiber 的链表。
- Hooks 通过
- 提交结果:
- 更新完成后,
workInProgressFiber 成为新的currentFiber。
- 更新完成后,
Hook:
- 核心结构
- 每个 Hook 以单向链表形式存储,
next指针连接下一个 Hook。 - 严格依赖调用顺序,确保多次渲染时顺序一致。
fiber.memoizedState属性保存了 当前fiber树的 Hooks链表的 头节点。
- 每个 Hook 以单向链表形式存储,
- 关键属性
memoizedState:当前状态(如state、effect对象、ref值)。baseState:更新计算的基准状态(用于中断恢复)。baseQueue:未完成的更新队列(持久化存储)。queue:新触发的更新队列(临时环形链表)。- ...
- 工作机制
- 挂载阶段
- 初始化 Hook 链表,绑定
hook.memoizedState(如useState(initialValue))。 - 创建
queue和dispatch函数(如setState) - 返回初始值和触发更新的函数(如
return [hook.memoizedState, dispatch])
- 初始化 Hook 链表,绑定
- 更新阶段
- 合并更新队列,将新触发的
queue合并到baseQueue。 - 基于
baseState遍历baseQueue,执行reducer计算出最新的state。 - 更新
workInProgressHook链表中的属性值,memoizedState、baseState、baseQueue。
- 合并更新队列,将新触发的
- 并发模式
- 优先级调度,高优先级更新可中断低优先级任务,优先渲染。
- 中断恢复,未完成的更新保留在
baseQueue,下次渲染继续处理。
- 挂载阶段
最后,我们再看看以下规则:
1. 不要在条件、循环和嵌套函数中使用 Hook?
因为 hook 链表需要保证一致性,updateWorkInProgressHook 会clone 旧fiber的 hook节点,给新fiber 使用,错误的顺序、数量 会导致 识别错乱、链表断裂的问题。
2. 不要在普通的 JavaScript 函数中调用 Hook?
Hook 的执行依赖于当前组件的 Fiber节点。
并且 ReactCurrentDispatcher.current 会被默认设置为 ContextOnlyDispatcher,使用 Hooks 时会报错。
结语
希望本文能对你有所帮助。持续更新前端知识,脚踏实地不水文,真的不关注一下吗~
写作不易,如果有收获希望 点赞、收藏 🌹
才疏学浅,如有问题或建议还望指教~