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
表示的字段, 都改为了lane
renderExpirationtime -> renderLanes
update.expirationTime -> update.lane
fiber.expirationTime -> fiber.lanes
fiber.childExpirationTime -> fiber.childLanes
root.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) 删除单个task
batchOfTasks &= ~task
// 2) 增加单个task
batchOfTasks |= 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 cases
const 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
模式为SyncLane
Blocking
模式为SyncBatchedLane
Concurrent
模式为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
和scheduler
2 包, 十分重要:
// 本函数每次更新和出调度任务的时候进行调用
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转换为SchedulerPriority
const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
// 2. 注册task
return Scheduler_scheduleCallback(priorityLevel, callback, options);
}
export function scheduleSyncCallback(callback: SchedulerCallback) {
if (syncQueue === null) {
syncQueue = [callback];
// 使用Scheduler_ImmediatePriority注册task
immediateQueueCallbackNode = 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 转换成 ReactPriorityLevel
export 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 转换成 SchedulerPriority
function 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 above
cancelTimeout(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;
在