前瞻

本文将介绍 useReducer 的核心机制,在阅读之前,推荐先大致了解 React Reconcile 机制,想要从本文获益的话,至少应当理解 React Fiber, Work Loop, 单向、循环链表数据结构的概念,关于 React 源码解析可参考我的另一篇文章:React v17 源码解析

由于 React Hook 的复杂度较高,读一遍文章就想建立起结构化记忆是很困难的,所以我推荐先了解以下核心机制,待熟悉后,再参考我给出的完整代码,把以下概念串联起来:

  1. Mount 与 Update 的区别
  2. ReactCurrentDispatcher 运行时注入
  3. Hook 对象与两个单向链表:currentHook & workInProgressHook
  4. Fiber 与 Hook 的联系
  5. Update Queue 与两个循环链表:base & pending
  6. Lane 模型
  7. 更新触发:dispatch
  8. useReducerOnMount & useReducerOnUpdate

Mount VS Update

React Hook 在组件 Mount 和 Update 阶段的逻辑是不同的,我们暂且称之为 useReducerOnMount 和useReducerOnUpdate,为了避免一上来就 Deep Dive 吓退读者,让我们暂且忽略二者的细节吧,对这两个函数仅做简单介绍,后文我会给出完整代码以供参考。

Mount 阶段,useReducer 所做的事,相对来说很简单:

  1. 创建 Hook 对象
  2. 构建单向链表 workInProgressHook
  3. 计算初始状态
  4. bind dispatch 回调函数

Update 阶段,稍微复杂一些:

  1. 克隆 Hook 对象
  2. 构建单向链表 currentHook
  3. 构建单向链表 workInProgressHook
  4. 检查前后两次渲染 Hook 调用次数是否一致
  5. 处理 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 字段函数组件里的含义
memoizedStatehooks 链表的头节点
updateQueueeffect 环形链表(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 的路径大致是:

  1. requestUpdateLane(fiber) 决定这次更新的 lane。Legacy 模式(ReactDOM.render)下所有更新都是 SyncLane;Concurrent 模式下会根据当前事件优先级(discrete / continuous / default)返回对应的 lane;
  2. createUpdate(lane) 把 lane 记在 update 上,enqueueUpdate 入队;
  3. markUpdateLaneFromFiberToRoot(fiber, lane) 从触发更新的 fiber 一路向上找到 root,把 lane 合并进路径上每个 fiber 的 lanes;
  4. markRootUpdated(root, lane) 把 lane 记到 root 的 pendingLanes;
  5. 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 了。

End of entry