likes
comments
collection
share

剖析React系列十- 调度<合并更新、优先级>

作者站长头像
站长
· 阅读数 67

本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React内部机制。 由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。

仓库地址

具体章节代码commit

系列文章:

  1. React实现系列一 - jsx
  2. 剖析React系列二-reconciler
  3. 剖析React系列三-打标记
  4. 剖析React系列四-commit
  5. 剖析React系列五-update流程
  6. 剖析React系列六-dispatch update流程
  7. 剖析React系列七-事件系统
  8. 剖析React系列八-同级节点diff
  9. 剖析React系列九-Fragment的部分实现逻辑

在之前的章节中,我们每一次触发更新 -> render -> commit都是同步触发, 所以多次触发更新会重复多次更新流程。 这和React真实情况是不符合的。

本章主要是多次触发更新,只进行一次更新流程。 也就是我们说的批处理Batched Updates

将多个更新合并为一次更新, 在实际运用中,有点类似我们防抖,节流。 在合并的时机上有2种可以选择:

  1. 宏任务
  2. 微任务

目前Vue以及Svelte都是通过微任务处理批量更新。对于React的批处理时机既有宏任务,又有微任务。本节中主要是针对微任务的批处理。分几个部分讲解

  1. 新增调度阶段
  2. update进行调整
  3. 实现优先级lane模型

新增调度阶段

在之前的实现中,我们的更新流程如图所示: 剖析React系列十- 调度<合并更新、优先级> 每一次点击都会执行

  • render阶段
  • commit阶段

为了能够控制多个触发更新,只进行一次更新流程,我们需要新增一个调度schedule阶段。将流程修改为如下图所示: 剖析React系列十- 调度<合并更新、优先级>

接下来我们看看如何去新增调度schedule阶段

修改update结构

回顾一下之前我们创建update的逻辑, 例如如下的三次dispatch流程, 在enqueueUpdate中,会被直接覆盖掉,很明显是不符合的。

const onClick = () => {  
  // 创建3个update  
  updateCount((count) => count + 1);  
  updateCount((count) => count + 1);  
  updateCount((count) => count + 1);  
};

// enqueueUpdate
export const enqueueUpdate = <State>(
  updateQueue: UpdateQueue<State>,
  update: Update<State>
) => {
  updateQueue.shared.pending = update;
};

我们需要保存三个update的逻辑,React内部通过一个环形保存更新队列。所以我们需要修改目前的逻辑。

enqueueUpdate

主要是存放update actions, 以便于之后消费update

/**
 * 更新update
 * @param {UpdateQueue<Action>} updateQueue
 * @param {Update<Action>} update
 */
export const enqueueUpdate = <State>(
  updateQueue: UpdateQueue<State>,
  update: Update<State>
) => {
  const pending = updateQueue.shared.pending;
  if (pending === null) {
    update.next = update;
  } else {
    update.next = pending.next;
    pending.next = update;
  }
  updateQueue.shared.pending = update;
};

例如:我们点击事件后,触发了a,b,c三个update action,简化一下会经过如下流程:

  1. 刚开始updateQueue.shared.pending为null, 此时相当于pending = a -> a
  2. 第二次b触发,pending !== null进入else分支,update.next = pending.next;执行, 等同于b.next = a, pending.next = update;执行,等同于a.next = b, 最后等于pending = b -> a -> b
  3. 第三次c触发, 首先c.next=a, 然后b.next = c。 最后等于pending = c -> a -> b -> c。一个环形列表。

剖析React系列十- 调度<合并更新、优先级>

优先级

React内部使用lane标识每一个引起更新update的优先级。不同的情况触发的更新,产生不同的lane。通过优先级机制,将触发更新的流程变成如下:

剖析React系列十- 调度<合并更新、优先级>

lane的产生

首先在创建update的时候,为每一个update都绑定一个优先级lane

function dispatchSetState<State>(
  fiber: FiberNode,
  updateQueue: UpdateQueue<State>,
  action: Action<State>
) {
  const lane = requestUpdateLane(); // 每一个更新设置一个lane(优先级)
  const update = createUpdate(action, lane); // 1. 创建update
  enqueueUpdate(updateQueue, update); //  2. 将更新放入队列中
  scheduleUpdateOnFiber(fiber, lane); // 3. 开始调度
}

创建lane优先级和update绑定后,并记录当前更新,开始执行scheduleUpdateOnFiber。在scheduleUpdateOnFiber中,我们需要对fiberRootNode进行改造。

对FiberRootNode的改造

主要是针对优先级的字段的补充,我们需要新增2个字段:

  • 代表所有未被消费的lane的集合: pendingLanes
  • 代表本次更新消费的lane: finishLane

剖析React系列十- 调度<合并更新、优先级> 上图是一个整体流程,如果我们一次触发了三次更新,由于每一个更新都有一个优先级,再进入调度阶段之前需要保存这些优先级。

我们将其保存到fiberRootNode.pendingLanes中。在每一次调度入口执行的时候,获取当前pendingLanes中最高优先级lane的任务,将其放入scheduleSyncCallback回调中等待微任务执行。

当主线程执行完成后,微任务开始执行最高优先级的任务,执行完成后,清除相应的lane

整体流程

简单的了解后,我们通过一个例子来解释整体的执行过程。现阶段我们只有一个优先级的任务lane。当执行dispatchState的时候,默认优先级为SyncLane = 0b0001;

三次点击整体流程如下:

  1. 触发dispatchState的事件,记录action之后,新建lane进入scheduleUpdateOnFiber
  2. 获取fiberRootNode并绑定新建的lanefiberRootNode.pendingLanes
  3. 进入ensureRootIsScheduled调度入口,获取fiberRootNode.pendingLanes中最高优先级lane
  4. 执行scheduleSyncCallback并传入performSyncWorkOnRoot(render入口)作为回调函数, 并赋值给syncQueue记录
  5. 执行scheduleMicroTask, 将flushSyncCallbacks推进微任务队列等待执行。
  6. 主线程继续执行dispatchState第一步,循环到第三次点击。此时syncQueue[performSyncWorkOnRoot,performSyncWorkOnRoot,performSyncWorkOnRoot]。主线程执行完成
  7. 开始执行微任务flushSyncCallbacks。开始render
  8. render以及commit之后,清理掉lane

剖析React系列十- 调度<合并更新、优先级>

消耗update

和之前的调和阶段不同,由于现在我们updateQueue.shared.pending指向的是一个环状列表,所以当执行到fiberHooks中的updateState的时候消费action的时候,我们需要将之前单一的操作,修改为遍历环状列表。

/**
 * 消费updateQueue(计算状态的最新值)
 */
export const processUpdateQueue = <State>(
  baseState: State,
  pendingUpdate: Update<State> | null,
  renderLane: Lane
): {
  memoizedState: State;
} => {
  const result: ReturnType<typeof processUpdateQueue<State>> = {
    memoizedState: baseState,
  };
  if (pendingUpdate !== null) {
    // 第一个update
    const first = pendingUpdate.next;
    let pending = pendingUpdate.next as Update<any>;
    do {
      const updateLane = pending.lane;
      if (updateLane === renderLane) {
        const action = pending.action;
        if (action instanceof Function) {
          // baseState 1 update (x) => 4x  -> memoizedState 4
          baseState = action(baseState);
        } else {
          // baseState 1 update 2 -> memoizedState 2
          baseState = action;
        }
      } else {
        if (__DEV__) {
          console.error("不应该进入processUpdateQueue");
        }
      }
      pending = pending.next as Update<any>;
    } while (pending !== first);
  }
  result.memoizedState = baseState;
  return result;
};

从第一个开始action开始遍历, 对应pendingUpdate.next。遍历完后,赋值给对应的memoizedState,作为新的渲染值。

总结

本章节主要是讲了基于微任务实现的多次更新合并成一次调度。不过目前只是针对了单一优先级,之后再单一优先级的基础下进行拓展。

下一节我们讲解react的下一个hook, useEffect的实现

转载自:https://juejin.cn/post/7197010875399749692
评论
请登录