useReducer - update

72 阅读3分钟

在具体说明之前,先放上一个例子。

function FunctionComponent() {
  const [number, setNumber] = React.useReducer(reducer, 0);
  return (
    <button
      onClick={() => {
        setNumber({ type: "add", payload: 1 });
        setNumber({ type: "add", payload: 2 });
        setNumber({ type: "add", payload: 3 });
      }}
    >
      {number}
    </button>
  );
}

根据上面这个例子,画出 mount 阶段完成后的完整 fiber 结构。如下图:

截屏2025-07-22 21.02.27.png

上图就是挂载阶段完成后的完整数据结构。从图中可以看到,这时的初始状态为 0。根据之前的学习,Fiber 节点有一个 alternate 属性指向他正在构建中的工作树(workInProgress)。而这个工作树就是需要实现的目标。

如下图:

截屏2025-07-22 21.13.39.png

目标就是实现这个数据结构并更新 hook 中的状态。

执行流程

之前已经说明挂载逻辑,接下来说明用户点击按钮,触发 setNumber 事件时的更新逻辑。

根据之前的说明了解到,setNumber 函数实际上是绑定了两个参数的 dispatchReducerAction 函数。这两个参数分别是 fiber 和 queue,也就是当前 fiber 和 对应的更新队列。setNumber 函数传入的为 update。这三个参数为一组,按顺序存储在 concurrentQueue 数组中。最后调用 scheduleUpdateOnFiber 函数。

scheduleUpdateOnFiber 函数的作用是渲染页面,调用这个函数也就意味着会开始重新构建 fiber 树,并且渲染。

根据一开始的 main.ts 示例代码,可以直接看对于函数式组件的处理逻辑。

/** 
 * 渲染函数组件
 * @param {*} current 老 fiber
 * @param {*} workInProgress 新 fiber
 * @param {*} Component 组件
 * @param {*} props 组件属性
 * @returns 虚拟DOM或者说 React 元素
 */
export function renderWithHooks(current, workInProgress, Component, props) {
  currentRenderingFiber = workInProgress;
  if (current !== null && current.memoizedState !== null) {
    // 更新
    ReactCurrentDispatcher.current = HooksDispatcherOnUpdate;
  } else {
    // 挂载
    ReactCurrentDispatcher.current = HooksDispatcherOnMount;
  }

  // 需要在函数组件执行前,给 ReactCurrentDispatcher.current 赋值
  const children = Component(props);
  currentRenderingFiber = null;
  workInProgressHook = null;
  return children;
}

在真正执行组件函数之前,需要判断当前渲染是更新或者是挂载,以挂载不同的 hook 处理函数。这里肯定是更新,所以现在的 useReducer 函数就是 HooksDispatcherOnUpdate.useReducer,这个函数就是 updateReducer 函数。

接下来执行组件函数,在组件函数中执行 useReducer 函数,也就是 updateReducer。

function updateReducer(reducer) {
  const hook = updateWorkInProgressHook();
  // 获取新 hook 的更新队列
  const queue = hook.queue;
  // 获取老的 hook
  const current = currentHook;
  // 获取将要生效的更新队列
  const pendingQueue = queue.pending;
  // 初始化一个新状态,取值为当前状态
  let newState = current.memoizedState;

  if (pendingQueue !== null) {
    queue.pending = null;
    const firstUpdate = pendingQueue.next;
    let update = firstUpdate;
    do {
      const action = update.action;
      newState = reducer(newState, action);
      update = update.next;
    } while (update !== null && update !== firstUpdate);
  }

  hook.memoizedState = newState;
  return [hook.memoizedState, queue.dispatch];
}


function updateWorkInProgressHook() {
  // 获取将要构建的新的 hook 的老 hook
  if (currentHook === null) {
    const current = currentRenderingFiber.alternate;
    currentHook = current.memoizedState;
  } else {
    currentHook = currentHook.next;
  }

  // 根据老 hook 创建新 hook
  const newHook = {
    memoizedState: currentHook.memoizedState,
    queue: currentHook.queue,
    next: null,
  };

  if (workInProgressHook === null) {
    currentRenderingFiber.memoizedState = workInProgressHook = newHook;
  } else {
    workInProgressHook = workInProgressHook.next = newHook;
  }

  return workInProgressHook;
}

在 updateReducer 函数中,首先要构建一个新的 hook 链表。所以首先调用 updateWorkInProgressHook 函数得到一个新 hook。这个时候,这个新 hook 的 memoizedState 和 queue 属性都复用的老 hook的。接下来需要根据 hook 中的更新队列,计算出新的 memoizedState。其实就是循环执行更新队列。最后返回新的状态。

最后组件函数执行完毕,拿到新返回的虚拟 DOM,这个时候,number 值为 6。

拿到返回的更新后的虚拟 DOM 后,进入正常的更新渲染逻辑,将更新渲染到页面中去。