前瞻
本文将介绍 useReducer 的核心机制,在阅读之前,推荐先大致了解 React Reconcile 机制,想要从本文获益的话,至少应当理解 React Fiber, Work Loop, 单向、循环链表数据结构的概念,关于 React 源码解析可参考我的另一篇文章:React v17 源码解析
由于 React Hook 的复杂度较高,读一遍文章就想建立起结构化记忆是很困难的,所以我推荐先了解以下核心机制,待熟悉后,再参考我给出的完整代码,把以下概念串联起来:
- Mount 与 Update 的区别
- ReactCurrentDispatcher 运行时注入
- Hook 对象与两个单向链表:currentHook & workInProgressHook
- Fiber 与 Hook 的联系
- Update Queue 与两个循环链表:base & pending
- Lane 模型
- 更新触发:dispatch
- useReducerOnMount & useReducerOnUpdate
Mount VS Update
React Hook 在组件 Mount 和 Update 阶段的逻辑是不同的,我们暂且称之为 useReducerOnMount 和useReducerOnUpdate,为了避免一上来就 Deep Dive 吓退读者,让我们暂且忽略二者的细节吧,对这两个函数仅做简单介绍,后文我会给出完整代码以供参考。
Mount 阶段,useReducer 所做的事,相对来说很简单:
- 创建 Hook 对象
- 构建单向链表 workInProgressHook
- 计算初始状态
- bind dispatch 回调函数
Update 阶段,稍微复杂一些:
- 克隆 Hook 对象
- 构建单向链表 currentHook
- 构建单向链表 workInProgressHook
- 检查前后两次渲染 Hook 调用次数是否一致
- 处理 Update Queue
ReactCurrentDispatcher 运行时注入
讲完了 useReducerOnMount 和useReducerOnUpdate,引出我们的第一个问题,这二者是如何导出为一个 Hook 函数 useReducer 的呢?
答案就是 ReactCurrentDispatcher 机制。我们先来看下这段代码,这是一段省略了部分函数调用的 React useReducer 源码:
// Dispatcher 包含了所有 Hook 的实现细节
const ReactCurrentDispatcher: any = {
current: null,
};
// 获取 Dispatcher
function resolveDispatcher() {
return ReactCurrentDispatcher.current;
}
// 最终版
function useMockReducer(reducer, initialArgs, init?) {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArgs, init);
}
有点感觉了吗?其实就是通过一个闭包全局变量 ReactCurrentDispatcher 实现的运行时注入。
具体来讲,在 reconcile 阶段,函数组件会通过 renderWithHook 函数进行 reconcile,ReactCurrentDispatcher.current 的引用替换就发生在此时此处:
// ...
if (current === null || current.memorizedState === null) {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
}
// ...
const child = Component();
// ...
上述代码段执行后,ReactCurrentDispatcher.current 就指向了 Mount 或 Update 阶段 Hooks 的底层实现,调用过程:renderWithHook -> Component -> useReducer -> ReactCurrentDispatcher.current.useReducer
关于 reconcile 与 renderWithHook 可以查看我的另一篇文章:React v17 源码解析
Hook 对象与 currentHook & workInProgressHook
在 React 17 的 ReactFiberHooks.js 里,一个 Hook 节点是这样定义的:
const hook = {
memoizedState: null, // 当前值:state / effect / [value, deps]
baseState: null, // 本轮渲染的基准值
baseQueue: null, // 留到下一轮的 update
queue: null, // 更新队列
next: null, // 下一个 Hook
};
hooks 链表由三个模块级变量配合维护:
let currentlyRenderingFiber = null; // 当前正在 render 的 fiber
let currentHook = null; // 指向 current 树上的 Hook
let workInProgressHook = null; // 指向 workInProgress 树上的 Hook
每次 renderWithHooks 开始时,React 会把 currentlyRenderingFiber 指向新的 workInProgress fiber,并把 currentHook、workInProgressHook 置空,然后调用组件函数。组件里的每次 useXxx 调用,本质上都是在按顺序“认领”链表上的下一个节点:
- 首次渲染(Mount):
mountWorkInProgressHook新建节点,第一个节点挂到fiber.memoizedState,后面的节点依次挂到前一个节点的next; - 更新(Update):
updateWorkInProgressHook同时推进 current 和 workInProgress 两条链表,把 current 上的节点克隆到 workInProgress 上,队列和状态都复用。
这也是“Hooks 不能写在条件语句里”的根本原因:没有 key,没有名字,React 唯一能对齐两次渲染中同一个 Hook 的依据就是调用顺序。顺序一变,状态就会错位。
React 17 的 updateWorkInProgressHook 大致如下(省略了 DEV 警告):
function updateWorkInProgressHook() {
// 1. 找 current 树上的下一个 Hook
let nextCurrentHook;
if (currentHook === null) {
const current = currentlyRenderingFiber.alternate;
nextCurrentHook = current !== null ? current.memoizedState : null;
} else {
nextCurrentHook = currentHook.next;
}
// 2. 找 workInProgress 树上的下一个 Hook
let nextWorkInProgressHook;
if (workInProgressHook === null) {
nextWorkInProgressHook = currentlyRenderingFiber.memoizedState;
} else {
nextWorkInProgressHook = workInProgressHook.next;
}
if (nextWorkInProgressHook !== null) {
// 3.1 已经存在(render 阶段更新导致组件函数被再次执行),直接复用
workInProgressHook = nextWorkInProgressHook;
currentHook = nextCurrentHook;
} else {
// 3.2 克隆 current Hook
invariant(
nextCurrentHook !== null,
"Rendered more hooks than during the previous render.",
);
currentHook = nextCurrentHook;
const newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
if (workInProgressHook === null) {
currentlyRenderingFiber.memoizedState = newHook;
} else {
workInProgressHook.next = newHook;
}
workInProgressHook = newHook;
}
return workInProgressHook;
}
注意 3.1 这条分支:当组件在 render 阶段又触发了一次更新(比如在函数体里调用 setState),React 会用一个 do...while 重新执行组件函数;此时 workInProgress 链上已经有上一次执行留下的 Hook,不能再克隆一遍,只能顺着走。
Fiber 与 Hook
把 Hook 挂到 Fiber 上之后,有几个字段需要区分清楚:
| Fiber 字段 | 函数组件里的含义 |
|---|---|
memoizedState | hooks 链表的头节点 |
updateQueue | effect 环形链表(useEffect / useLayoutEffect) |
alternate | 双缓冲的另一棵 Fiber 树 |
lanes | 该 Fiber 上待处理的更新优先级 |
类组件的 memoizedState 存的是 state 对象,updateQueue 存的是 update 队列;函数组件正好相反:memoizedState 被 hooks 链表占用,updateQueue 留给 effect。读源码时这里最容易串。
Fiber 采用双缓冲:页面上正在显示的是 current 树,render 阶段在 workInProgress 树上进行;两棵树通过 alternate 互相指向。commit 结束后 root.current = finishedWork,下一次更新时角色互换。Hook 也一样,current 和 workInProgress 上的两份链表是一一对应的,updateWorkInProgressHook 做的就是“沿着两条链表同步前进”。
function renderWithHooks(
current,
workInProgress,
Component,
props,
secondArg,
nextRenderLanes,
) {
renderLanes = nextRenderLanes;
currentlyRenderingFiber = workInProgress;
// 每次 render 前清空,避免残留上一次的 hooks / effect
workInProgress.memoizedState = null;
workInProgress.updateQueue = null;
workInProgress.lanes = NoLanes;
ReactCurrentDispatcher.current =
current === null || current.memoizedState === null
? HooksDispatcherOnMount
: HooksDispatcherOnUpdate;
const children = Component(props, secondArg);
// render 阶段更新:重新执行组件函数
if (didScheduleRenderPhaseUpdateDuringThisPass) {
do {
didScheduleRenderPhaseUpdateDuringThisPass = false;
// ...
children = Component(props, secondArg);
} while (didScheduleRenderPhaseUpdateDuringThisPass);
}
finishRenderingHooks();
return children;
}
finishRenderingHooks 会把 dispatcher 切回 ContextOnlyDispatcher。如果在组件外调用 Hook,拿到的就是这个会直接抛错的 dispatcher——这也是“Invalid hook call”报错的来源。同时它还会检查本次渲染的 Hook 数量是否少于上一次,少于就报 Rendered fewer hooks than expected。
effect 的路径单独说一句:useEffect 在 render 阶段只做两件事——创建 effect 对象、把它 append 到 workInProgress.updateQueue 这个环形链表上;真正的执行发生在 commit 阶段,由 commitHookEffectListMount 遍历链表调用 create(),返回值保存到 effect.destroy,下次执行或卸载时先调用 destroy。
Update Queue 与 queue.base & queue.pending
useReducer 的 queue 在 React 17 里长这样:
const queue = {
pending: null, // 普通 update 的循环链表,指向最后一个 update
interleaved: null, // 交互事件中产生的 update,render 前会合并进 pending
lanes: NoLanes, // 队列上待处理的优先级
dispatch: null, // dispatchAction.bind(null, fiber, queue)
lastRenderedReducer: reducer,
lastRenderedState: state, // 上一次渲染后的状态,用于 eagerState
};
pending 是一个环形单链表:pending 指向最后一个 update,pending.next 是第一个。这样入队只要 O(1),并且随时能拿到队尾,方便做“最后一个 update 的 eagerState”判断。
为什么还需要 baseQueue / baseState?因为 lanes 允许一次 render 只处理一部分优先级的更新。假设队列里有三个更新:
A(Default) -> B(Transition) -> C(Default)
当前这次 render 只处理 Default lane:
- A 命中,
newState = reducer(newState, A.action); - B 的 lane 不在
renderLanes里,跳过。React 会把 B 克隆进baseQueue,并把baseState固定为“处理 A 之后、B 之前”的状态; - C 虽然命中,但为了保持顺序,也会被克隆进
baseQueue,同时仍然应用到newState上。
于是这一轮结束后:hook.memoizedState 是 A、C 之后的状态,hook.baseState 是 A 之后的状态,hook.baseQueue 里是 B、C。下一次 Transition 渲染时,从 baseState 开始重放 B、C,顺序不会乱。
React 17 的 updateReducer 主循环(简化):
do {
const updateLane = update.lane;
if (!isSubsetOfLanes(renderLanes, updateLane)) {
// 优先级不够:克隆到 baseQueue,等下一轮
const clone = { ...update, next: null };
if (newBaseQueueLast === null) {
newBaseQueueFirst = newBaseQueueLast = clone;
newBaseState = newState;
} else {
newBaseQueueLast = newBaseQueueLast.next = clone;
}
markSkippedUpdateLanes(updateLane);
} else {
// 优先级命中:执行 reducer
if (newBaseQueueLast !== null) {
const clone = { ...update, next: null };
newBaseQueueLast = newBaseQueueLast.next = clone;
}
newState = update.eagerReducer === reducer
? update.eagerState
: reducer(newState, update.action);
}
update = update.next;
} while (update !== null && update !== first);
markSkippedUpdateLanes 会把跳过的 lane 记到 root 的 skippedLanes 上,保证这些更新不会因为一直排不上队而饿死。这段逻辑也是 useReducer 里最值得单独写一篇的部分,文章里的 Mock 为了可读性把它简化掉了。
Lane 模型
React 17 用 lanes 取代了 16 的 expirationTime。lane 是一个 31 位的 bitmask,一个 lane 表示一个优先级,多个 lane 组成 lanes 集合:
export const SyncLane = 0b0000000000000000000000000000001;
export const InputContinuousLane = 0b0000000000000000000000000000100;
export const DefaultLane = 0b0000000000000000000000000010000;
// TransitionLanes 是一段连续的位(最多 16 条),IdleLane、OffscreenLane 在最高位
和 expirationTime 相比,lanes 的好处是能同时表达多个优先级:renderLanes 表示“本次 render 处理哪些 lane”,fiber.lanes 表示“这个 fiber 上还有哪些 lane 待处理”,而不是只能记一个数字。
一次更新从触发到进入调度,lane 的路径大致是:
requestUpdateLane(fiber)决定这次更新的 lane。Legacy 模式(ReactDOM.render)下所有更新都是SyncLane;Concurrent 模式下会根据当前事件优先级(discrete / continuous / default)返回对应的 lane;createUpdate(lane)把 lane 记在 update 上,enqueueUpdate入队;markUpdateLaneFromFiberToRoot(fiber, lane)从触发更新的 fiber 一路向上找到 root,把 lane 合并进路径上每个 fiber 的lanes;markRootUpdated(root, lane)把 lane 记到 root 的pendingLanes;ensureRootIsScheduled(root)调用getNextLanes从pendingLanes里挑出本次要渲染的最高优先级 lane 集合,再映射成 Scheduler 的优先级。
几个工具函数在源码里出现频率很高:
includesSomeLane(a, b) // a 和 b 有交集
isSubsetOfLanes(a, b) // a 是 b 的子集(本次 render 是否覆盖该 update)
mergeLanes(a, b) // 合并两个 lanes
getHighestPriorityLane(lanes) // 取最低位的 lane
lanes 还解决了“同一事件里连续 setState”的合并问题:同一个事件上下文里产生的更新会拿到相同的 lane,render 时一次性处理;不同优先级的更新则可以在不同轮次里分别处理,高优先级先走,低优先级留在 baseQueue 里等下一轮。
触发组件更新:dispatchAction
useReducer 返回的 dispatch,就是 dispatchAction.bind(null, currentlyRenderingFiber, queue)。它闭包住了“哪个 fiber”和“哪个队列”,所以身份是稳定的——这也是为什么把 dispatch 从 useEffect 的依赖数组里去掉不会出问题。
React 17 的 dispatchAction 主干:
function dispatchAction(fiber, queue, action) {
// 1. 计算本次更新的优先级
const lane = requestUpdateLane(fiber);
// 2. 创建 update 节点
const update = {
lane,
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 3. render 阶段更新:不排队,标记后让组件函数重新执行
if (fiber === currentlyRenderingFiber || fiber.alternate === currentlyRenderingFiber) {
didScheduleRenderPhaseUpdateDuringThisPass = true;
// ... 存进 renderPhaseUpdates,稍后统一处理
return;
}
// 4. 普通更新:插入 queue.pending 环形链表
const pending = queue.pending;
if (pending === null) {
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
// 5. eagerState:没有待处理更新时,先算一次,值没变就直接返回
const alternate = fiber.alternate;
if (fiber.lanes === NoLanes && (alternate === null || alternate.lanes === NoLanes)) {
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
const currentState = queue.lastRenderedState;
const eagerState = lastRenderedReducer(currentState, action);
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
if (Object.is(eagerState, currentState)) {
return; // 状态没变,不调度 render
}
}
}
// 6. 打标记、调度 render
scheduleUpdateOnFiber(fiber, lane, eventTime);
}
几个细节:
- eagerState 是一把双刃剑。它让
setState(prev => prev)这类“值没变”的更新直接短路,省掉一次 render;代价是 reducer 可能被执行两次(dispatch 时一次、render 时一次),所以 reducer 必须是纯函数。 - render 阶段更新走的是另一条路:在组件函数体里调用 dispatch,会设置
didScheduleRenderPhaseUpdateDuringThisPass,renderWithHooks随后用do...while重新执行组件函数,直到没有新的 render 阶段更新。 scheduleUpdateOnFiber负责把 lane 标记到 root,并根据模式决定同步执行还是交给 Scheduler:
function scheduleUpdateOnFiber(fiber, lane, eventTime) {
const root = markUpdateLaneFromFiberToRoot(fiber, lane);
markRootUpdated(root, lane, eventTime);
if (lane === SyncLane) {
// Legacy:同步 render
performSyncWorkOnRoot(root);
} else {
ensureRootIsScheduled(root, eventTime);
}
}
在 Legacy 模式下,如果 dispatch 发生在 React 事件回调里,executionContext 不是 NoContext,这次 render 会被推迟到事件处理结束后统一 flushSyncCallbackQueue,这就是“事件里连续 setState 只渲染一次”的来源。
Final Boss:useReducerOnMount & useReducerOnUpdate
Mount 阶段真正执行的是 mountReducer(useState 是 useReducer 的一个特例):
function mountReducer(reducer, initialArg, init) {
const hook = mountWorkInProgressHook();
let initialState;
if (init !== undefined) {
initialState = init(initialArg);
} else {
initialState = initialArg;
}
hook.memoizedState = hook.baseState = initialState;
const queue = (hook.queue = {
pending: null,
interleaved: null,
lanes: NoLanes,
dispatch: null,
lastRenderedReducer: reducer,
lastRenderedState: initialState,
});
const dispatch = (queue.dispatch = dispatchAction.bind(
null,
currentlyRenderingFiber,
queue,
));
return [hook.memoizedState, dispatch];
}
Update 阶段是 updateReducer:
function updateReducer(reducer, initialArg, init) {
const hook = updateWorkInProgressHook();
const queue = hook.queue;
queue.lastRenderedReducer = reducer;
// 1. 把 pending 合并进 baseQueue
const current = currentHook;
let baseQueue = current.baseQueue;
const pendingQueue = queue.pending;
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;
}
// 2. 遍历 baseQueue,跳过优先级不够的 update
if (baseQueue !== null) {
// ... 上一节那段 do...while
hook.memoizedState = newState;
hook.baseState = newBaseState;
hook.baseQueue = newBaseQueueLast;
queue.lastRenderedState = newState;
}
// 3. dispatch 还是 Mount 时那一个
return [hook.memoizedState, queue.dispatch];
}
把这三段连起来看,一次完整的 useReducer 就是:
Mount: renderWithHooks -> useReducer -> resolveDispatcher -> mountReducer
-> mountWorkInProgressHook -> 创建 queue -> bind dispatch
Update: renderWithHooks -> useReducer -> updateReducer
-> updateWorkInProgressHook -> 合并 pending -> 计算 newState
Dispatch: dispatchAction -> requestUpdateLane -> 入队 -> eagerState
-> scheduleUpdateOnFiber -> render -> commit
文章里的 Mock 保留了这条主线,省略了三块内容:lane 过滤与 baseQueue 的完整处理、render 阶段更新、effect 类 Hook 的链表。把这三块补上,就是 React 17 ReactFiberHooks.js 的真实规模了。
完整 Mock 代码
// 当前正在渲染的 fiber 节点
let currentlyRenderingFiber;
// 当前正在处理的 Hook 节点
let workInProgressHook;
// 上次 render 后处理完毕的 Hook 节点
let currentHook;
// Hook 节点初始化
function mountWorkInProgressHook() {
// 1. 初始化 hook 对象
const hook = {
memorizedState: null,
next: null,
};
// 2. 维护 hooks 链表
if (workInProgressHook === null) {
// 2.1 首次执行,把当前 hook 节点,保存到当前正在渲染的 fiber 节点上
currentlyRenderingFiber.memorizedState = hook;
} else {
// 2.2 非首次执行,维护 hooks 链表即可:当前 hook 节点 append 到 hooks 链表
workInProgressHook.next = hook;
}
// 3. 将 hook 节点标记为正在处理
workInProgressHook = hook;
return hook;
}
// Hook 节点复用
function updateWorkInProgressHook() {
// 1. 确定 currentHook 节点来源
let nextCurrentHook;
if (currentHook == null) {
// 1.1 说明是首次 Update,直接复用 alternate fiber 上保存的 hook 节点(此处做了简化,实际还要判断 alternate 是否为空)
nextCurrentHook = currentlyRenderingFiber.alternate.memorizedState;
} else {
// 1.2 说明不是首次 Update,那么直接复用 currentHook.next
nextCurrentHook = currentHook.next;
}
// 2. 确定 workInProgressHook 节点来源
let nextWorkInProgressHook;
if (workInProgressHook == null) {
// 2.1 说明这是组件中调用的首个 Hook(仔细体会跟 Case 1.1 的区别),直接复用 current fiber 上保存的 hook 节点(一般来说,就是 alternate 上的 hook 节点)
nextWorkInProgressHook = currentlyRenderingFiber.memorizedState;
} else {
// 2.2 说明这并非组件中调用的首个 Hook,那么直接复用 workInProgressHook.next
nextWorkInProgressHook = workInProgressHook.next;
}
// 3. 找到下个要处理的 Hook
if (nextWorkInProgressHook !== null) {
// 3.1 既然 nextWorkInProgress 有值,那就将 nextWorkInProgress 标记为处理中
// 3.1.1 把 next 指向的 hook 标记为处理中
workInProgressHook = nextWorkInProgressHook;
// 3.1.2 再移动链表指针 next 到下个节点
nextWorkInProgressHook = workInProgressHook.next;
} else {
// 3.2 说明 workInProgressHook 链表还没创建,此时克隆 currentHook 节点
// 3.2.1 对于 Hook 来说,current 和 workInProgress 总是成对出现,此处既然 workInProgress 存在,
// 那么 current 也必须存在,否则就说明这次将要处理的 hook 数量比上次 render 已处理完毕的 hook 数量要多,给出警告!
if (nextCurrentHook === null) {
throw new Error("Rendered more hooks than during the previous render.");
}
// 3.2.2 克隆 currentHook 节点
const newHook = {
memoizedState: currentHook.memoizedState,
baseState: currentHook.baseState,
baseQueue: currentHook.baseQueue,
queue: currentHook.queue,
next: null,
};
// 3.2.3 维护 Hook 链表结构
if (workInProgressHook == null) {
// 3.2.3.1 newHook 置为头结点
currentlyRenderingFiber.memorizedState = newHook;
} else {
// 3.2.3.2 newHook 附加到链表尾
workInProgressHook.next = newHook;
}
// 3.2.4 标记 newHook 为处理中
workInProgressHook = newHook;
}
// 4. 移动 currentHook 链表指针到下个节点(此处简化,实际代码是在步骤 3 中完成的)
currentHook = nextCurrentHook;
// 5. 返回被标记为处理中的 hook 节点
return workInProgressHook;
}
// 触发更新时会调用该函数,类组件的 setState 也会调用该函数,重新 render、commit
function scheduleUpdateOnFiber(fiber) {
// ...
}
// 判断是否为交互事件中触发的更新
function isInterleavedUpdate(fiber): boolean {
// ...
return true;
}
// 交互事件中触发的更新会被维护在一个队列中
function pushInterleavedQueue(queue) {
// ...
}
// dispatch 函数,每次执行生成一个 update 节点,这些节点会以循环链表形式,被维护在 Update Queue 的 pending 或 interleaved 属性上
function dispatchAction(fiber, queue, action) {
// 1. 初始化 update 节点
const update: any = {
action,
eagerReducer: null,
eagerState: null,
next: null,
};
// 2. 维护 updates 循环链表
if (isInterleavedUpdate(fiber)) {
// 2.1 交互事件中触发的更新,维护到 queue.interleaved,这是一个循环链表
const interleaved = queue.interleaved;
if (interleaved === null) {
// This is the first update. Create a circular list.
update.next = update;
// At the end of the current render, this queue's interleaved updates will
// be transfered to the pending queue.
pushInterleavedQueue(queue);
} else {
update.next = interleaved.next;
interleaved.next = update;
}
queue.interleaved = update;
} else {
// 2.2 普通更新,维护到 queue.pending,这是一个循环链表
const pending = queue.pending;
if (pending === null) {
// This is the first update. Create a circular list.
update.next = update;
} else {
update.next = pending.next;
pending.next = update;
}
queue.pending = update;
}
// 3. 执行 reducer
const lastRenderedReducer = queue.lastRenderedReducer;
if (lastRenderedReducer !== null) {
const currentState = queue.lastRenderedState;
// 3.1 reducer 就是这里执行的
const eagerState = lastRenderedReducer(currentState, action);
// 3.2 初始化 update 节点的 eagerReducer 和 eagerState
update.eagerReducer = lastRenderedReducer;
update.eagerState = eagerState;
// 3.3 优化:状态没有改变,立即返回,不会进入后续的 render、commit
if (Object.is(currentState, eagerState)) {
return;
}
}
// 4. 更新状态,重新 render、commit
scheduleUpdateOnFiber(fiber);
}
// Mount 时调用的 useReducer
function useReducerOnMount(reducer, initialArgs, init?) {
// 1. hook 节点初始化
const hook = mountWorkInProgressHook();
// 2. 计算初始状态
let memorizedState;
if (init) {
// 2.1 三参数用法
memorizedState = init(initialArgs);
} else {
// 2.2 双参数用法,useState 本质就是执行的这条逻辑分支
memorizedState = initialArgs;
}
hook.memorizedState = memorizedState;
// 3. 初始化 Updates Queue
const queue: any = {
// 3.1 这个是普通 update 的循环链表
pending: null,
// 3.2 这个是交互事件触发的 update 的循环链表
interleaved: null,
// 3.3 这个就是传入的 reducer 函数
lastRenderedReducer: reducer,
// 3.4 这个就是当前状态
lastRenderedState: memorizedState,
};
// 4. 初始化 dispatch 函数,dispatch 会保持对 currentlyRenderingFiber 和 queue 的引用
queue.dispatch = dispatchAction.bind(null, currentlyRenderingFiber, queue);
return [hook.memorizedState, queue.dispatch];
}
// Update 时调用的 useReducer
function useReducerOnUpdate(reducer, initialArgs, init?) {
// 1. Hook 复用,无法复用时创建一个新的
const hook = updateWorkInProgressHook();
// 2. 更新 reducer
hook.queue.lastRenderedReducer = reducer;
// 3. 合并 baseQueue 和 pendingQueue
if (hook.queue.pending !== null) {
// ... 很复杂,考虑单独去写一篇《Update Queue 机制》
}
// 4. 遍历 baseQueue,进行状态计算
if (currentHook.baseQueue !== null) {
// ... 很复杂,考虑单独去写一篇《Update Queue 机制》
}
// 5. 返回的还是 Mount 时创建的 dispatch 回调,
// 这也是为什么 setState/dispatch 可以安全地从 useEffect/useMemo/useCallback 的依赖列表中移除的原因
return [hook.memorizedState, hook.queue.dispatch];
}
// Dispatcher 包含了所有 Hook 的实现细节
const ReactCurrentDispatcher: any = {
current: null,
};
// 获取 Dispatcher
function resolveDispatcher() {
return ReactCurrentDispatcher.current;
}
// 最终版
function useMockReducer(reducer, initialArgs, init?) {
const dispatcher = resolveDispatcher();
return dispatcher.useReducer(reducer, initialArgs, init);
}
// Hooks 宿主对象分为两种,一种是 Mount 时调用,一种 Update 时调用,他的作用就是统一函数名
const HooksDispatcherOnMount = {
useReducer: useReducerOnMount,
// ... other hooks implements
};
const HooksDispatcherOnUpdate = {
useReducer: useReducerOnUpdate,
// ... other hooks implements
};
// 当前正在处理的 work unit,这个概念不展开了,去看 React Work Loop 就明白了
let workInProgress;
// reconcile 函数组件时会调用该函数
function renderWithHooks(current: any, Component: any) {
// ignore...
// 1. 标记当前 work unit 为正在渲染的 fiber 节点,dispatchAction 将保持对该对象的引用(通过bind实现)
currentlyRenderingFiber = workInProgress;
// 2. ReactCurrentDispatcher 就在这里被赋值为 Hooks 宿主对象
if (current === null || current.memorizedState === null) {
ReactCurrentDispatcher.current = HooksDispatcherOnMount;
} else {
ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
}
// 3. 执行函数组件获取 React Element,我们的 useMockReducer 此时就会被执行
const child = Component();
// ignore...
}
// 调用起来和实际 useReducer 类似
function Foo() {
// 首次调用 useMockReducer,将初始化一个 hooks 链表,新建 hook 节点作为头结点
const [state, setState] = useMockReducer(
(state, action) => {
return {
...state,
...action,
};
},
{ count: 0 }
);
// 第二次调用 useMockReducer,再新建一个 hook 节点,添加到 hooks 链表尾
const [state_, setState_] = useMockReducer(
(state, action) => {
return {
...state,
...action,
};
},
{ count: 0 }
);
// ...
}
写在最后
useReducer 是理解 Hooks 的最好入口:useState 只是它的一个特例,useMemo / useCallback 复用的是同一套 hook 链表,useEffect 只是把数据换成了 effect 对象。把 hook 链表、update 环形队列、lanes 这三样东西串起来,再回头看 useState,基本就是同一段逻辑换了个 reducer。
下一站可以读 ReactFiberHooks.js 里的 updateEffect 和 commitHookEffectListMount,那是 Hooks 的另一半;也可以顺着 dispatchAction 继续读 scheduleUpdateOnFiber,就接上 Work Loop 了。