剖析React系列十- 调度<合并更新、优先级>
本系列是讲述从0开始实现一个react18的基本版本。通过实现一个基本版本,让大家深入了解React内部机制。
由于React源码通过Mono-repo 管理仓库,我们也是用pnpm提供的workspaces来管理我们的代码仓库,打包我们使用rollup进行打包。
系列文章:
- React实现系列一 - jsx
- 剖析React系列二-reconciler
- 剖析React系列三-打标记
- 剖析React系列四-commit
- 剖析React系列五-update流程
- 剖析React系列六-dispatch update流程
- 剖析React系列七-事件系统
- 剖析React系列八-同级节点diff
- 剖析React系列九-Fragment的部分实现逻辑
在之前的章节中,我们每一次触发更新 -> render -> commit都是同步触发, 所以多次触发更新会重复多次更新流程。 这和React真实情况是不符合的。
本章主要是多次触发更新,只进行一次更新流程。 也就是我们说的批处理Batched Updates
将多个更新合并为一次更新, 在实际运用中,有点类似我们防抖,节流。 在合并的时机上有2种可以选择:
- 宏任务
- 微任务
目前Vue以及Svelte都是通过微任务处理批量更新。对于React的批处理时机既有宏任务,又有微任务。本节中主要是针对微任务的批处理。分几个部分讲解
- 新增调度阶段
- 对
update进行调整 - 实现优先级
lane模型
新增调度阶段
在之前的实现中,我们的更新流程如图所示:
每一次点击都会执行
render阶段commit阶段
为了能够控制多个触发更新,只进行一次更新流程,我们需要新增一个调度schedule阶段。将流程修改为如下图所示:

接下来我们看看如何去新增调度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,简化一下会经过如下流程:
- 刚开始
updateQueue.shared.pending为null, 此时相当于pending = a -> a - 第二次
b触发,pending !== null进入else分支,update.next = pending.next;执行, 等同于b.next = a,pending.next = update;执行,等同于a.next = b, 最后等于pending = b -> a -> b - 第三次
c触发, 首先c.next=a, 然后b.next = c。 最后等于pending = c -> a -> b -> c。一个环形列表。

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

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
上图是一个整体流程,如果我们一次触发了三次更新,由于每一个更新都有一个优先级,再进入调度阶段之前需要保存这些优先级。
我们将其保存到fiberRootNode.pendingLanes中。在每一次调度入口执行的时候,获取当前pendingLanes中最高优先级lane的任务,将其放入scheduleSyncCallback回调中等待微任务执行。
当主线程执行完成后,微任务开始执行最高优先级的任务,执行完成后,清除相应的lane。
整体流程
简单的了解后,我们通过一个例子来解释整体的执行过程。现阶段我们只有一个优先级的任务lane。当执行dispatchState的时候,默认优先级为SyncLane = 0b0001;。
三次点击整体流程如下:
- 触发
dispatchState的事件,记录action之后,新建lane进入scheduleUpdateOnFiber中 - 获取
fiberRootNode并绑定新建的lane到fiberRootNode.pendingLanes中 - 进入
ensureRootIsScheduled调度入口,获取fiberRootNode.pendingLanes中最高优先级lane - 执行
scheduleSyncCallback并传入performSyncWorkOnRoot(render入口)作为回调函数, 并赋值给syncQueue记录 - 执行
scheduleMicroTask, 将flushSyncCallbacks推进微任务队列等待执行。 - 主线程继续执行
dispatchState第一步,循环到第三次点击。此时syncQueue为[performSyncWorkOnRoot,performSyncWorkOnRoot,performSyncWorkOnRoot]。主线程执行完成 - 开始执行微任务
flushSyncCallbacks。开始render render以及commit之后,清理掉lane。

消耗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