title: 优先级管理
React 中的优先级管理
React是一个声明式, 高效且灵活的用于构建用户界面的 JavaScript 库. React 团队一直致力于实现高效渲染, 其中有 2 个十分有名的演讲:
- 2017 年 Lin Clark 的演讲中介绍了
fiber架构和可中断渲染. - 2018 年 Dan 在 JSConf 冰岛的演讲进一步介绍了时间切片(
time slicing)和异步渲染(suspense)等特性.
演讲中所展示的可中断渲染,时间切片(time slicing),异步渲染(suspense)等特性, 在源码中得以实现都依赖于优先级管理.
预备知识
在深入分析之前, 再次回顾一下(reconciler 运作流程):

react 内部对于优先级的管理, 根据其源码所在不同的包, 可以分为 2 种类型:
- 渲染优先级: 位于
react-reconciler包, 也就是Lane(车道模型). - 调度优先级: 位于
scheduler包.
Lane (车道模型)
英文单词
lane翻译成中文表示”车道, 航道”的意思, 所以很多文章都将Lanes模型称为车道模型
Lane模型的源码在ReactFiberLane.js, 源码中大量使用了位运算(有关位运算的讲解, 可以参考React 算法之位运算).
首先引入作者对Lane的解释(相应的 pr), 这里简单概括如下:
Lane类型被定义为二进制变量, 利用了位掩码的特性, 在频繁的时候占用内存少, 计算速度快.Lane和Lanes就是单数和复数的关系, 代表单个任务的定义为Lane, 代表多个任务的定义为Lanes
Lane是对于expirationTime的重构, 以前使用expirationTime表示的字段, 都改为了lanerenderExpirationtime -> renderLanesupdate.expirationTime -> update.lanefiber.expirationTime -> fiber.lanesfiber.childExpirationTime -> fiber.childLanesroot.firstPendingTime and root.lastPendingTime -> fiber.pendingLanes
使用
Lanes模型相比expirationTime模型的优势:Lanes把任务优先级从批量任务中分离出来, 可以更方便的判断单个任务与批量任务的优先级是否重叠.// 判断: 单task与batchTask的优先级是否重叠//1. 通过expirationTime判断const isTaskIncludedInBatch = priorityOfTask >= priorityOfBatch;//2. 通过Lanes判断const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;// 当同时处理一组任务, 该组内有多个任务, 且每个任务的优先级不一致// 1. 如果通过expirationTime判断. 需要维护一个范围(在Lane重构之前, 源码中就是这样比较的)const isTaskIncludedInBatch =taskPriority <= highestPriorityInRange &&taskPriority >= lowestPriorityInRange;//2. 通过Lanes判断const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
Lanes使用单个 32 位二进制变量即可代表多个不同的任务, 也就是说一个变量即可代表一个组(group), 如果要在一个 group 中分离出单个 task, 非常容易.在
expirationTime模型设计之初, react 体系中还没有Suspense 异步渲染的概念. 现在有如下场景: 有 3 个任务, 其优先级A > B > C, 正常来讲只需要按照优先级顺序执行就可以了. 但是现在情况变了: A 和 C 任务是CPU密集型, 而 B 是IO密集型(Suspense 会调用远程 api, 算是 IO 任务), 即A(cup) > B(IO) > C(cpu). 此时的需求需要将任务B从 group 中分离出来, 先处理 cpu 任务A和C.// 从group中删除或增加task//1. 通过expirationTime实现// 0) 维护一个链表, 按照单个task的优先级顺序进行插入// 1) 删除单个task(从链表中删除一个元素)task.prev.next = task.next;// 2) 增加单个task(需要对比当前task的优先级, 插入到链表正确的位置上)let current = queue;while (task.expirationTime >= current.expirationTime) {current = current.next;}task.next = current.next;current.next = task;// 3) 比较task是否在group中const isTaskIncludedInBatch =taskPriority <= highestPriorityInRange &&taskPriority >= lowestPriorityInRange;
// 2. 通过Lanes实现// 1) 删除单个taskbatchOfTasks &= ~task// 2) 增加单个taskbatchOfTasks |= task// 3) 比较task是否在group中const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;```通过上述伪代码, 可以看到`Lanes`的优越性, 运用起来代码量少, 简洁高效.
Lanes是一个不透明的类型, 只能在ReactFiberLane.js这个模块中维护. 如果要在其他文件中使用, 只能通过ReactFiberLane.js中提供的工具函数来使用.
分析车道模型的源码(ReactFiberLane.js中), 可以得到如下结论:
- 可以使用的比特位一共有 31 位(为什么? 可以参考React 算法之位运算中的说明).
- 共定义了18 种车道(
Lane/Lanes)变量, 每一个变量占有 1 个或多个比特位, 分别定义为Lane和Lanes类型. - 每一种车道(
Lane/Lanes)都有对应的优先级, 所以源码中定义了 18 种优先级(LanePriority). - 占有低位比特位的
Lane变量对应的优先级越高- 最高优先级为
SyncLanePriority对应的车道为SyncLane = 0b0000000000000000000000000000001. - 最高优先级为
OffscreenLanePriority对应的车道为OffscreenLane = 0b1000000000000000000000000000000.
- 最高优先级为
优先级使用
现在正式进入正题, 把优先级机制对应到reconciler 运作流程中, 那么它创建于第一步(输入), 贯穿于整个输入到输出的过程. 后文将以reconciler 运作流程的 4 个阶段为时间线, 逐一分析每一个步骤中关于优先级的运用情况.
输入阶段
通过启动过程一文的解读, 我们知道react应用初始化之后, 会经过updateContainer函数, 最后进入scheduleUpdateOnFiber函数.
注意scheduleUpdateOnFiber(fiber: Fiber,lane: Lane,eventTime: number)函数签名中的第 2 个参数lane: Lane就是贯穿全局的优先级, 它是Lane类型, 实际上是一个二级制变量.
再往前推一步, lane实际上是在updateContainer函数中首次创建(优先级的源头所在).
// ... 省略部分无关代码export function updateContainer(element: ReactNodeList,container: OpaqueRoot,parentComponent: ?React$Component<any, any>,callback: ?Function,): Lane {const current = container.current;// 1. 获取当前时间戳const eventTime = requestEventTime();// 2. 创建一个优先级变量(车道模型)const lane = requestUpdateLane(current);// 3. 根据车道优先级, 创建update对象, 并加入fiber.updateQueue.pending队列const update = createUpdate(eventTime, lane);update.payload = { element };enqueueUpdate(current, update);// 4. 正式进入`输入`环节scheduleUpdateOnFiber(current, lane, eventTime);return lane;}
首先分析requestEventTime()函数, 顺着调用栈依次跟踪, 最后调用了scheduler包中的getCurrentTime(), 返回了从react应用开始运行, 到本次调用经过的绝对时间(即performance.now())
然后跟踪requestUpdateLane函数:
//... 省略部分代码export function requestUpdateLane(fiber: Fiber): Lane {// Special casesconst mode = fiber.mode;if ((mode & BlockingMode) === NoMode) {// Legacy 模式return (SyncLane: Lane);} else if ((mode & ConcurrentMode) === NoMode) {// Blocking 模式return getCurrentPriorityLevel() === ImmediateSchedulerPriority? (SyncLane: Lane): (SyncBatchedLane: Lane);}// Concurrent 模式if (currentEventWipLanes === NoLanes) {currentEventWipLanes = workInProgressRootIncludedLanes;}const schedulerPriority = getCurrentPriorityLevel();let lane;const schedulerLanePriority = schedulerPriorityToLanePriority(schedulerPriority,);lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);return lane;}
在requestUpdateLane中会根据不同的模式, 返回不同的优先级, 默认情况如下:
Legacy模式为SyncLaneBlocking模式为SyncBatchedLaneConcurrent模式为DefaultLanes
回到updateContainer函数, 接下来使用了requestUpdateLane返回的优先级, 创建update对象, 并添加到updateQueue队列中.
此处可以回顾React 应用中的高频对象章节中已经介绍过Update与UpdateQueue对象以及它们的数据结构.
需要注意,update.payload指向最终 DOM 树将要挂载的节点(div#root).
在updateContainer函数的最后, 调用了scheduleUpdateOnFiber(current, lane, eventTime)进入到输入阶段(reconciler 运作流程)的必经函数. 由于本节的主题是优先级管理, 所以我们重点跟踪lane 和 eventTime这 2 个参数的用途.
// ... 省略部分无关代码export function scheduleUpdateOnFiber(fiber: Fiber,lane: Lane,eventTime: number,) {const root = markUpdateLaneFromFiberToRoot(fiber, lane);if (lane === SyncLane) {// Legacy 模式下 lane === SyncLane才成立if ((executionContext & LegacyUnbatchedContext) !== NoContext &&(executionContext & (RenderContext | CommitContext)) === NoContext) {// 直接进行`fiber构造`performSyncWorkOnRoot(root);} else {// 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`ensureRootIsScheduled(root, eventTime);}} else {// Blocking 和 Concurrent模式ensureRootIsScheduled(root, eventTime);}}
在scheduleUpdateOnFiber的主干逻辑中, 只有Legacy模式下lane === SyncLane才成立, 才会直接进入performSyncWorkOnRoot, 否则必然调用ensureRootIsScheduled进入到注册调度任务. 注意eventTime被传入了ensureRootIsScheduled.
整理出输入阶段优先级相关的逻辑:
- 创建一个优先级变量
lane - 根据车道优先级
lane, 创建update对象, 并加入fiber.updateQueue.pending队列
调度阶段
逻辑来到了ensureRootIsScheduled中(源码地址), 这个函数串联了react-reconciler和scheduler2 包, 十分重要:
// 本函数每次更新和出调度任务的时候进行调用function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {// 1. 前半部分: 判断是否需要注册新的调度const existingCallbackNode = root.callbackNode;// 1.1 检查starve, 将已过期的车道(lane), 添加到root.expiredLanes中markStarvedLanesAsExpired(root, currentTime);// 1.2 获取当前最需要被调度的车道(Lanes)const nextLanes = getNextLanes(root,root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,);// 1.3 获取需要调度的车道的优先级等级const newCallbackPriority = returnNextLanesPriority();// 1.4 如果没有任何车道需要调度, 则退出调度if (nextLanes === NoLanes) {if (existingCallbackNode !== null) {// 取消已经进入调度的任务cancelCallback(existingCallbackNode);root.callbackNode = null;root.callbackPriority = NoLanePriority;}return;}// 1.5 如果已经有调度任务了, 则比较old任务与new任务的优先级等级if (existingCallbackNode !== null) {const existingCallbackPriority = root.callbackPriority;if (existingCallbackPriority === newCallbackPriority) {// 1.5.1 优先级相同, 表示可以复用old调度任务, 退出循环return;}// 1.5.2 优先级不同, 则取消old调度任务cancelCallback(existingCallbackNode);}// 2. 后半部分: 注册调度任务let newCallbackNode;// 2.1 注册task并设置回调函数if (newCallbackPriority === SyncLanePriority) {// legacy 模式newCallbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root),);} else if (newCallbackPriority === SyncBatchedLanePriority) {// blocking 模式newCallbackNode = scheduleCallback(ImmediateSchedulerPriority,performSyncWorkOnRoot.bind(null, root),);} else {// concurrent 模式const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority,);newCallbackNode = scheduleCallback(schedulerPriorityLevel,performConcurrentWorkOnRoot.bind(null, root),);}// 2.2 在FiberRoot对象上面设置一些标记, 用于再次调用ensureRootIsScheduled时作为比较.root.callbackPriority = newCallbackPriority;root.callbackNode = newCallbackNode;}
ensureRootIsScheduled的逻辑比较清晰(源码中每一步都有英文注释), 主要分为 2 部分:
- 前半部分: 确定是否需要注册新的调度(如果无需新的调度, 会退出函数)
- 后半部分: 注册调度任务
在前半部分中:
- 函数
getNextLanes返回了需要调度的车道(nextLanes) - 函数
returnNextLanesPriority返回了需要调度的车道(nextLanes)中, 所占用的最高的优先级. - 函数
lanePriorityToSchedulerPriority把lanePriority转换成SchedulerPriority
后半部分调用scheduleSyncCallback 或 scheduleCallback:
export function scheduleCallback(reactPriorityLevel: ReactPriorityLevel,callback: SchedulerCallback,options: SchedulerCallbackOptions | void | null,) {// 1. 把reactPriorityLevel转换为SchedulerPriorityconst priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);// 2. 注册taskreturn Scheduler_scheduleCallback(priorityLevel, callback, options);}export function scheduleSyncCallback(callback: SchedulerCallback) {if (syncQueue === null) {syncQueue = [callback];// 使用Scheduler_ImmediatePriority注册taskimmediateQueueCallbackNode = Scheduler_scheduleCallback(Scheduler_ImmediatePriority,flushSyncCallbackQueueImpl,);} else {syncQueue.push(callback);}return fakeCallbackNode;}
可见scheduleSyncCallback 和 scheduleCallback均调用Scheduler_scheduleCallback, 唯一不同的就是优先级.
由于此处涉及到react-reconciler包和scheduler包的衔接, 尤其关注其中优先级的转换. 通过梳理, 在task注册过程中, 一共包含了 3 种优先级.
LanePriority: 属于react-reconciler包, 定义于ReactFiberLane.js(见源码).export const SyncLanePriority: LanePriority = 15;export const SyncBatchedLanePriority: LanePriority = 14;const InputDiscreteHydrationLanePriority: LanePriority = 13;export const InputDiscreteLanePriority: LanePriority = 12;// .....const OffscreenLanePriority: LanePriority = 1;export const NoLanePriority: LanePriority = 0;
reactPriorityLevel, 属于react-reconciler包, 定义于SchedulerWithReactIntegration.js中(见源码).export const ImmediatePriority: ReactPriorityLevel = 99;export const UserBlockingPriority: ReactPriorityLevel = 98;export const NormalPriority: ReactPriorityLevel = 97;export const LowPriority: ReactPriorityLevel = 96;export const IdlePriority: ReactPriorityLevel = 95;// NoPriority is the absence of priority. Also React-only.export const NoPriority: ReactPriorityLevel = 90;
SchedulerPriority, 属于scheduler包, 定义于SchedulerPriorities.js中(见源码).export const NoPriority = 0;export const ImmediatePriority = 1;export const UserBlockingPriority = 2;export const NormalPriority = 3;export const LowPriority = 4;export const IdlePriority = 5;
- 与
fiber构造过程相关的优先级(如fiber.updateQueue,fiber.lanes)都使用LanePriority. - 与
scheduler调度中心相关的优先级使用SchedulerPriority. LanePriority与SchedulerPriority通过ReactPriorityLevel进行转换
在SchedulerWithReactIntegration.js中, 转换关系如下:
// 把 SchedulerPriority 转换成 ReactPriorityLevelexport function getCurrentPriorityLevel(): ReactPriorityLevel {switch (Scheduler_getCurrentPriorityLevel()) {case Scheduler_ImmediatePriority:return ImmediatePriority;case Scheduler_UserBlockingPriority:return UserBlockingPriority;case Scheduler_NormalPriority:return NormalPriority;case Scheduler_LowPriority:return LowPriority;case Scheduler_IdlePriority:return IdlePriority;default:invariant(false, 'Unknown priority level.');}}// 把 ReactPriorityLevel 转换成 SchedulerPriorityfunction reactPriorityToSchedulerPriority(reactPriorityLevel) {switch (reactPriorityLevel) {case ImmediatePriority:return Scheduler_ImmediatePriority;case UserBlockingPriority:return Scheduler_UserBlockingPriority;case NormalPriority:return Scheduler_NormalPriority;case LowPriority:return Scheduler_LowPriority;case IdlePriority:return Scheduler_IdlePriority;default:invariant(false, 'Unknown priority level.');}}
在ReactFiberLane.js中, 转换关系如下:
export function schedulerPriorityToLanePriority(schedulerPriorityLevel: ReactPriorityLevel,): LanePriority {switch (schedulerPriorityLevel) {case ImmediateSchedulerPriority:return SyncLanePriority;// ... 省略部分代码default:return NoLanePriority;}}export function lanePriorityToSchedulerPriority(lanePriority: LanePriority,): ReactPriorityLevel {switch (lanePriority) {case SyncLanePriority:case SyncBatchedLanePriority:return ImmediateSchedulerPriority;// ... 省略部分代码default:invariant(false,'Invalid update priority: %s. This is a bug in React.',lanePriority,);}}
理清楚 3 种优先级的关系之后, 回到Scheduler_scheduleCallback函数, 逻辑完全进入了scheduler包. 对于scheduler包内部的运转, 放在scheduler 调度机制中详细解读.
此处整理出调度阶段优先级相关的逻辑:
- 获取当前最需要被调度的车道(
Lanes)及其占用的最高优先级newCallbackPriority - 注册调度任务
task, 传入的参数是当前的优先级和回调函数- 注册
task的过程中, 需要优先级转换(LanePriority --> ReactPriorityLevel --> SchedulerPriority) - 在
scheduler包中维护的task队列, 使用的优先级类型是SchedulerPriority task队列会按照过期时间expirationTime从小到大进行堆排序, 优先级越高的, 过期时间越小
- 注册
- 调度中心(
scheduler包)负责在下一个浏览器事件循环依次执行task队列中的回调任务(详见scheduler 调度机制)
fiber 树构造阶段
在调度阶段, 执行完react-reconciler包的最后一个函数scheduleCallback之后, 即在scheduler包中注册了task. 此后,控制react运行时的主动权转移到了scheduler包中, react-reconciler包只需要被动等待回调.
调度中心依次执行task队列中的回调任务, 当执行task.callback时, 逻辑再次回到react-reconciler包, 此时被调用的函数是performSyncWorkOnRoot(只在Legacy模式)或performConcurrentWorkOnRoot
在performSyncWorkOnRoot和performConcurrentWorkOnRoot的调用链路中, 本节先关心优先级的使用.
其中prepareFreshStack函数:
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {root.finishedWork = null;root.finishedLanes = NoLanes;const timeoutHandle = root.timeoutHandle;if (timeoutHandle !== noTimeout) {// The root previous suspended and scheduled a timeout to commit a fallback// state. Now that we have additional work, cancel the timeout.root.timeoutHandle = noTimeout;// $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check abovecancelTimeout(timeoutHandle);}if (workInProgress !== null) {let interruptedWork = workInProgress.return;while (interruptedWork !== null) {unwindInterruptedWork(interruptedWork);interruptedWork = interruptedWork.return;}}workInProgressRoot = root;workInProgress = createWorkInProgress(root.current, null);workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;workInProgressRootExitStatus = RootIncomplete;workInProgressRootFatalError = null;workInProgressRootSkippedLanes = NoLanes;workInProgressRootUpdatedLanes = NoLanes;workInProgressRootPingedLanes = NoLanes;}
从函数签名来看prepareFreshStack(root: FiberRoot, lanes: Lanes),prepareFreshStack的字面意思是刷新栈帧, 在方法体中, 主要逻辑是重置全局变量. 其中有关lanes的操作
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
在
