title: 优先级管理

React 中的优先级管理

React是一个声明式, 高效且灵活的用于构建用户界面的 JavaScript 库. React 团队一直致力于实现高效渲染, 其中有 2 个十分有名的演讲:

  1. 2017 年 Lin Clark 的演讲中介绍了fiber架构和可中断渲染.
  2. 2018 年 Dan 在 JSConf 冰岛的演讲进一步介绍了时间切片(time slicing)和异步渲染(suspense)等特性.

演讲中所展示的可中断渲染,时间切片(time slicing),异步渲染(suspense)等特性, 在源码中得以实现都依赖于优先级管理.

预备知识

在深入分析之前, 再次回顾一下(reconciler 运作流程):

React 中的优先级管理 - 图1

react 内部对于优先级的管理, 根据其源码所在不同的包, 可以分为 2 种类型:

  1. 渲染优先级: 位于react-reconciler包, 也就是Lane(车道模型).
  2. 调度优先级: 位于scheduler包.

Lane (车道模型)

英文单词lane翻译成中文表示”车道, 航道”的意思, 所以很多文章都将Lanes模型称为车道模型

Lane模型的源码在ReactFiberLane.js, 源码中大量使用了位运算(有关位运算的讲解, 可以参考React 算法之位运算).

首先引入作者对Lane的解释(相应的 pr), 这里简单概括如下:

  1. Lane类型被定义为二进制变量, 利用了位掩码的特性, 在频繁的时候占用内存少, 计算速度快.
    • LaneLanes就是单数和复数的关系, 代表单个任务的定义为Lane, 代表多个任务的定义为Lanes
  2. Lane是对于expirationTime的重构, 以前使用expirationTime表示的字段, 都改为了lane
    1. renderExpirationtime -> renderLanes
    2. update.expirationTime -> update.lane
    3. fiber.expirationTime -> fiber.lanes
    4. fiber.childExpirationTime -> fiber.childLanes
    5. root.firstPendingTime and root.lastPendingTime -> fiber.pendingLanes
  3. 使用Lanes模型相比expirationTime模型的优势:

    1. Lanes把任务优先级从批量任务中分离出来, 可以更方便的判断单个任务与批量任务的优先级是否重叠.

      1. // 判断: 单task与batchTask的优先级是否重叠
      2. //1. 通过expirationTime判断
      3. const isTaskIncludedInBatch = priorityOfTask >= priorityOfBatch;
      4. //2. 通过Lanes判断
      5. const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
      6. // 当同时处理一组任务, 该组内有多个任务, 且每个任务的优先级不一致
      7. // 1. 如果通过expirationTime判断. 需要维护一个范围(在Lane重构之前, 源码中就是这样比较的)
      8. const isTaskIncludedInBatch =
      9. taskPriority <= highestPriorityInRange &&
      10. taskPriority >= lowestPriorityInRange;
      11. //2. 通过Lanes判断
      12. const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
    2. 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.

      1. // 从group中删除或增加task
      2. //1. 通过expirationTime实现
      3. // 0) 维护一个链表, 按照单个task的优先级顺序进行插入
      4. // 1) 删除单个task(从链表中删除一个元素)
      5. task.prev.next = task.next;
      6. // 2) 增加单个task(需要对比当前task的优先级, 插入到链表正确的位置上)
      7. let current = queue;
      8. while (task.expirationTime >= current.expirationTime) {
      9. current = current.next;
      10. }
      11. task.next = current.next;
      12. current.next = task;
      13. // 3) 比较task是否在group中
      14. const isTaskIncludedInBatch =
      15. taskPriority <= highestPriorityInRange &&
      16. taskPriority >= lowestPriorityInRange;
  1. // 2. 通过Lanes实现
  2. // 1) 删除单个task
  3. batchOfTasks &= ~task
  4. // 2) 增加单个task
  5. batchOfTasks |= task
  6. // 3) 比较task是否在group中
  7. const isTaskIncludedInBatch = (task & batchOfTasks) !== 0;
  8. ```
  9. 通过上述伪代码, 可以看到`Lanes`的优越性, 运用起来代码量少, 简洁高效.
  1. Lanes是一个不透明的类型, 只能在ReactFiberLane.js这个模块中维护. 如果要在其他文件中使用, 只能通过ReactFiberLane.js中提供的工具函数来使用.

分析车道模型的源码(ReactFiberLane.js中), 可以得到如下结论:

  1. 可以使用的比特位一共有 31 位(为什么? 可以参考React 算法之位运算中的说明).
  2. 共定义了18 种车道(Lane/Lanes)变量, 每一个变量占有 1 个或多个比特位, 分别定义为LaneLanes类型.
  3. 每一种车道(Lane/Lanes)都有对应的优先级, 所以源码中定义了 18 种优先级(LanePriority).
  4. 占有低位比特位的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函数中首次创建(优先级的源头所在).

  1. // ... 省略部分无关代码
  2. export function updateContainer(
  3. element: ReactNodeList,
  4. container: OpaqueRoot,
  5. parentComponent: ?React$Component<any, any>,
  6. callback: ?Function,
  7. ): Lane {
  8. const current = container.current;
  9. // 1. 获取当前时间戳
  10. const eventTime = requestEventTime();
  11. // 2. 创建一个优先级变量(车道模型)
  12. const lane = requestUpdateLane(current);
  13. // 3. 根据车道优先级, 创建update对象, 并加入fiber.updateQueue.pending队列
  14. const update = createUpdate(eventTime, lane);
  15. update.payload = { element };
  16. enqueueUpdate(current, update);
  17. // 4. 正式进入`输入`环节
  18. scheduleUpdateOnFiber(current, lane, eventTime);
  19. return lane;
  20. }

首先分析requestEventTime()函数, 顺着调用栈依次跟踪, 最后调用了scheduler包中的getCurrentTime(), 返回了从react应用开始运行, 到本次调用经过的绝对时间(即performance.now())

然后跟踪requestUpdateLane函数:

  1. //... 省略部分代码
  2. export function requestUpdateLane(fiber: Fiber): Lane {
  3. // Special cases
  4. const mode = fiber.mode;
  5. if ((mode & BlockingMode) === NoMode) {
  6. // Legacy 模式
  7. return (SyncLane: Lane);
  8. } else if ((mode & ConcurrentMode) === NoMode) {
  9. // Blocking 模式
  10. return getCurrentPriorityLevel() === ImmediateSchedulerPriority
  11. ? (SyncLane: Lane)
  12. : (SyncBatchedLane: Lane);
  13. }
  14. // Concurrent 模式
  15. if (currentEventWipLanes === NoLanes) {
  16. currentEventWipLanes = workInProgressRootIncludedLanes;
  17. }
  18. const schedulerPriority = getCurrentPriorityLevel();
  19. let lane;
  20. const schedulerLanePriority = schedulerPriorityToLanePriority(
  21. schedulerPriority,
  22. );
  23. lane = findUpdateLane(schedulerLanePriority, currentEventWipLanes);
  24. return lane;
  25. }

requestUpdateLane中会根据不同的模式, 返回不同的优先级, 默认情况如下:

  • Legacy模式为SyncLane
  • Blocking模式为SyncBatchedLane
  • Concurrent模式为DefaultLanes

回到updateContainer函数, 接下来使用了requestUpdateLane返回的优先级, 创建update对象, 并添加到updateQueue队列中.

此处可以回顾React 应用中的高频对象章节中已经介绍过UpdateUpdateQueue对象以及它们的数据结构. 需要注意,update.payload指向最终 DOM 树将要挂载的节点(div#root).

updateContainer函数的最后, 调用了scheduleUpdateOnFiber(current, lane, eventTime)进入到输入阶段(reconciler 运作流程)的必经函数. 由于本节的主题是优先级管理, 所以我们重点跟踪lane 和 eventTime这 2 个参数的用途.

  1. // ... 省略部分无关代码
  2. export function scheduleUpdateOnFiber(
  3. fiber: Fiber,
  4. lane: Lane,
  5. eventTime: number,
  6. ) {
  7. const root = markUpdateLaneFromFiberToRoot(fiber, lane);
  8. if (lane === SyncLane) {
  9. // Legacy 模式下 lane === SyncLane才成立
  10. if (
  11. (executionContext & LegacyUnbatchedContext) !== NoContext &&
  12. (executionContext & (RenderContext | CommitContext)) === NoContext
  13. ) {
  14. // 直接进行`fiber构造`
  15. performSyncWorkOnRoot(root);
  16. } else {
  17. // 注册调度任务, 经过`Scheduler`包的调度, 间接进行`fiber构造`
  18. ensureRootIsScheduled(root, eventTime);
  19. }
  20. } else {
  21. // Blocking 和 Concurrent模式
  22. ensureRootIsScheduled(root, eventTime);
  23. }
  24. }

scheduleUpdateOnFiber的主干逻辑中, 只有Legacy模式下lane === SyncLane才成立, 才会直接进入performSyncWorkOnRoot, 否则必然调用ensureRootIsScheduled进入到注册调度任务. 注意eventTime被传入了ensureRootIsScheduled.

整理出输入阶段优先级相关的逻辑:

  1. 创建一个优先级变量lane
  2. 根据车道优先级lane, 创建update对象, 并加入fiber.updateQueue.pending队列

调度阶段

逻辑来到了ensureRootIsScheduled中(源码地址), 这个函数串联了react-reconcilerscheduler2 包, 十分重要:

  1. // 本函数每次更新和出调度任务的时候进行调用
  2. function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
  3. // 1. 前半部分: 判断是否需要注册新的调度
  4. const existingCallbackNode = root.callbackNode;
  5. // 1.1 检查starve, 将已过期的车道(lane), 添加到root.expiredLanes中
  6. markStarvedLanesAsExpired(root, currentTime);
  7. // 1.2 获取当前最需要被调度的车道(Lanes)
  8. const nextLanes = getNextLanes(
  9. root,
  10. root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
  11. );
  12. // 1.3 获取需要调度的车道的优先级等级
  13. const newCallbackPriority = returnNextLanesPriority();
  14. // 1.4 如果没有任何车道需要调度, 则退出调度
  15. if (nextLanes === NoLanes) {
  16. if (existingCallbackNode !== null) {
  17. // 取消已经进入调度的任务
  18. cancelCallback(existingCallbackNode);
  19. root.callbackNode = null;
  20. root.callbackPriority = NoLanePriority;
  21. }
  22. return;
  23. }
  24. // 1.5 如果已经有调度任务了, 则比较old任务与new任务的优先级等级
  25. if (existingCallbackNode !== null) {
  26. const existingCallbackPriority = root.callbackPriority;
  27. if (existingCallbackPriority === newCallbackPriority) {
  28. // 1.5.1 优先级相同, 表示可以复用old调度任务, 退出循环
  29. return;
  30. }
  31. // 1.5.2 优先级不同, 则取消old调度任务
  32. cancelCallback(existingCallbackNode);
  33. }
  34. // 2. 后半部分: 注册调度任务
  35. let newCallbackNode;
  36. // 2.1 注册task并设置回调函数
  37. if (newCallbackPriority === SyncLanePriority) {
  38. // legacy 模式
  39. newCallbackNode = scheduleSyncCallback(
  40. performSyncWorkOnRoot.bind(null, root),
  41. );
  42. } else if (newCallbackPriority === SyncBatchedLanePriority) {
  43. // blocking 模式
  44. newCallbackNode = scheduleCallback(
  45. ImmediateSchedulerPriority,
  46. performSyncWorkOnRoot.bind(null, root),
  47. );
  48. } else {
  49. // concurrent 模式
  50. const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
  51. newCallbackPriority,
  52. );
  53. newCallbackNode = scheduleCallback(
  54. schedulerPriorityLevel,
  55. performConcurrentWorkOnRoot.bind(null, root),
  56. );
  57. }
  58. // 2.2 在FiberRoot对象上面设置一些标记, 用于再次调用ensureRootIsScheduled时作为比较.
  59. root.callbackPriority = newCallbackPriority;
  60. root.callbackNode = newCallbackNode;
  61. }

ensureRootIsScheduled的逻辑比较清晰(源码中每一步都有英文注释), 主要分为 2 部分:

  1. 前半部分: 确定是否需要注册新的调度(如果无需新的调度, 会退出函数)
  2. 后半部分: 注册调度任务

在前半部分中:

  • 函数getNextLanes返回了需要调度的车道(nextLanes)
  • 函数returnNextLanesPriority返回了需要调度的车道(nextLanes)中, 所占用的最高的优先级.
  • 函数lanePriorityToSchedulerPrioritylanePriority转换成SchedulerPriority

后半部分调用scheduleSyncCallback 或 scheduleCallback:

  1. export function scheduleCallback(
  2. reactPriorityLevel: ReactPriorityLevel,
  3. callback: SchedulerCallback,
  4. options: SchedulerCallbackOptions | void | null,
  5. ) {
  6. // 1. 把reactPriorityLevel转换为SchedulerPriority
  7. const priorityLevel = reactPriorityToSchedulerPriority(reactPriorityLevel);
  8. // 2. 注册task
  9. return Scheduler_scheduleCallback(priorityLevel, callback, options);
  10. }
  11. export function scheduleSyncCallback(callback: SchedulerCallback) {
  12. if (syncQueue === null) {
  13. syncQueue = [callback];
  14. // 使用Scheduler_ImmediatePriority注册task
  15. immediateQueueCallbackNode = Scheduler_scheduleCallback(
  16. Scheduler_ImmediatePriority,
  17. flushSyncCallbackQueueImpl,
  18. );
  19. } else {
  20. syncQueue.push(callback);
  21. }
  22. return fakeCallbackNode;
  23. }

可见scheduleSyncCallback 和 scheduleCallback均调用Scheduler_scheduleCallback, 唯一不同的就是优先级.

由于此处涉及到react-reconciler包和scheduler包的衔接, 尤其关注其中优先级的转换. 通过梳理, 在task注册过程中, 一共包含了 3 种优先级.

  1. LanePriority: 属于react-reconciler包, 定义于ReactFiberLane.js(见源码).

    1. export const SyncLanePriority: LanePriority = 15;
    2. export const SyncBatchedLanePriority: LanePriority = 14;
    3. const InputDiscreteHydrationLanePriority: LanePriority = 13;
    4. export const InputDiscreteLanePriority: LanePriority = 12;
    5. // .....
    6. const OffscreenLanePriority: LanePriority = 1;
    7. export const NoLanePriority: LanePriority = 0;
  2. reactPriorityLevel, 属于react-reconciler包, 定义于SchedulerWithReactIntegration.js中(见源码).

    1. export const ImmediatePriority: ReactPriorityLevel = 99;
    2. export const UserBlockingPriority: ReactPriorityLevel = 98;
    3. export const NormalPriority: ReactPriorityLevel = 97;
    4. export const LowPriority: ReactPriorityLevel = 96;
    5. export const IdlePriority: ReactPriorityLevel = 95;
    6. // NoPriority is the absence of priority. Also React-only.
    7. export const NoPriority: ReactPriorityLevel = 90;
  3. SchedulerPriority, 属于scheduler包, 定义于SchedulerPriorities.js中(见源码).

    1. export const NoPriority = 0;
    2. export const ImmediatePriority = 1;
    3. export const UserBlockingPriority = 2;
    4. export const NormalPriority = 3;
    5. export const LowPriority = 4;
    6. export const IdlePriority = 5;
  • fiber构造过程相关的优先级(如fiber.updateQueue,fiber.lanes)都使用LanePriority.
  • scheduler调度中心相关的优先级使用SchedulerPriority.
  • LanePrioritySchedulerPriority通过ReactPriorityLevel进行转换

SchedulerWithReactIntegration.js, 转换关系如下:

  1. // 把 SchedulerPriority 转换成 ReactPriorityLevel
  2. export function getCurrentPriorityLevel(): ReactPriorityLevel {
  3. switch (Scheduler_getCurrentPriorityLevel()) {
  4. case Scheduler_ImmediatePriority:
  5. return ImmediatePriority;
  6. case Scheduler_UserBlockingPriority:
  7. return UserBlockingPriority;
  8. case Scheduler_NormalPriority:
  9. return NormalPriority;
  10. case Scheduler_LowPriority:
  11. return LowPriority;
  12. case Scheduler_IdlePriority:
  13. return IdlePriority;
  14. default:
  15. invariant(false, 'Unknown priority level.');
  16. }
  17. }
  18. // 把 ReactPriorityLevel 转换成 SchedulerPriority
  19. function reactPriorityToSchedulerPriority(reactPriorityLevel) {
  20. switch (reactPriorityLevel) {
  21. case ImmediatePriority:
  22. return Scheduler_ImmediatePriority;
  23. case UserBlockingPriority:
  24. return Scheduler_UserBlockingPriority;
  25. case NormalPriority:
  26. return Scheduler_NormalPriority;
  27. case LowPriority:
  28. return Scheduler_LowPriority;
  29. case IdlePriority:
  30. return Scheduler_IdlePriority;
  31. default:
  32. invariant(false, 'Unknown priority level.');
  33. }
  34. }

ReactFiberLane.js, 转换关系如下:

  1. export function schedulerPriorityToLanePriority(
  2. schedulerPriorityLevel: ReactPriorityLevel,
  3. ): LanePriority {
  4. switch (schedulerPriorityLevel) {
  5. case ImmediateSchedulerPriority:
  6. return SyncLanePriority;
  7. // ... 省略部分代码
  8. default:
  9. return NoLanePriority;
  10. }
  11. }
  12. export function lanePriorityToSchedulerPriority(
  13. lanePriority: LanePriority,
  14. ): ReactPriorityLevel {
  15. switch (lanePriority) {
  16. case SyncLanePriority:
  17. case SyncBatchedLanePriority:
  18. return ImmediateSchedulerPriority;
  19. // ... 省略部分代码
  20. default:
  21. invariant(
  22. false,
  23. 'Invalid update priority: %s. This is a bug in React.',
  24. lanePriority,
  25. );
  26. }
  27. }

理清楚 3 种优先级的关系之后, 回到Scheduler_scheduleCallback函数, 逻辑完全进入了scheduler包. 对于scheduler包内部的运转, 放在scheduler 调度机制中详细解读.

此处整理出调度阶段优先级相关的逻辑:

  1. 获取当前最需要被调度的车道(Lanes)及其占用的最高优先级newCallbackPriority
  2. 注册调度任务task, 传入的参数是当前的优先级和回调函数
    • 注册task的过程中, 需要优先级转换(LanePriority --> ReactPriorityLevel --> SchedulerPriority)
    • scheduler包中维护的task队列, 使用的优先级类型是SchedulerPriority
    • task队列会按照过期时间expirationTime从小到大进行堆排序, 优先级越高的, 过期时间越小
  3. 调度中心(scheduler包)负责在下一个浏览器事件循环依次执行task队列中的回调任务(详见scheduler 调度机制)

fiber 树构造阶段

调度阶段, 执行完react-reconciler包的最后一个函数scheduleCallback之后, 即在scheduler包中注册了task. 此后,控制react运行时的主动权转移到了scheduler包中, react-reconciler包只需要被动等待回调.

调度中心依次执行task队列中的回调任务, 当执行task.callback时, 逻辑再次回到react-reconciler包, 此时被调用的函数是performSyncWorkOnRoot(只在Legacy模式)performConcurrentWorkOnRoot

performSyncWorkOnRootperformConcurrentWorkOnRoot的调用链路中, 本节先关心优先级的使用.

其中prepareFreshStack函数:

  1. function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
  2. root.finishedWork = null;
  3. root.finishedLanes = NoLanes;
  4. const timeoutHandle = root.timeoutHandle;
  5. if (timeoutHandle !== noTimeout) {
  6. // The root previous suspended and scheduled a timeout to commit a fallback
  7. // state. Now that we have additional work, cancel the timeout.
  8. root.timeoutHandle = noTimeout;
  9. // $FlowFixMe Complains noTimeout is not a TimeoutID, despite the check above
  10. cancelTimeout(timeoutHandle);
  11. }
  12. if (workInProgress !== null) {
  13. let interruptedWork = workInProgress.return;
  14. while (interruptedWork !== null) {
  15. unwindInterruptedWork(interruptedWork);
  16. interruptedWork = interruptedWork.return;
  17. }
  18. }
  19. workInProgressRoot = root;
  20. workInProgress = createWorkInProgress(root.current, null);
  21. workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
  22. workInProgressRootExitStatus = RootIncomplete;
  23. workInProgressRootFatalError = null;
  24. workInProgressRootSkippedLanes = NoLanes;
  25. workInProgressRootUpdatedLanes = NoLanes;
  26. workInProgressRootPingedLanes = NoLanes;
  27. }

从函数签名来看prepareFreshStack(root: FiberRoot, lanes: Lanes),prepareFreshStack的字面意思是刷新栈帧, 在方法体中, 主要逻辑是重置全局变量. 其中有关lanes的操作

  1. workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;

输出

总结