剖析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