深入浅出react(优先级管理)
本文正在参加「金石计划 . 瓜分6万现金大奖」 大家好,我是六六。这是深入浅出react的第四篇,主要讲述react中的优先级管理,本章从以下六个问题开始:
- 为什么需要优先级?
- react有几种优先级关系?
- lane和位运算符的关系
- 优先级机制如何设计?
- React运行时的优先级机制
- 什么是饥饿任务问题?
1.为什么react需要优先级?
我们先举一个例子,在同步模式
下进行更新操作,从虚拟dom到真实dom
,最终渲染到页面上,假如在这个过程中,用户在进行了其他操作,这个操作是无法被立即响应的。因为我们这个同步的操作是无法打断的。用户事件触发的更新被阻塞,体验是非常不好的。这个时候就有了异步模式
,我们希望能把用户触发的任务放在前面,及时去响应用户的事件。
所以优先级的最终目的,高优先级任务先执行,低优先级任务后执行。
2.react有几种优先级关系?
React
内部对于优先级
的管理, 根据其功能的不同, 可以分为 3 种类型:
Lane
优先级(LanePriority
)- 调度优先级(
SchedulerPriority
) - 优先级等级,负责上述两种的转换。(
ReactPriorityLevel
)
2.1Lane优先级
lane
的翻译为车道,我们可以用赛道的概念理解他。lane
优先级有31
个,我们可以用31
位的二进制值去表示,值的每一位代表一条赛道对应一个lane
优先级。赛道越靠前,优先级越高。为什么要用二进制呢?因为Lane
类型被定义为二进制变量, 利用了位掩码
的特性, 在频繁运算的时候占用内存少
, 计算速度快
。
export const NoLanes: Lanes = 0b0000000000000000000000000000000;
export const NoLane: Lane = 0b0000000000000000000000000000000;
export const SyncLane: Lane = 0b0000000000000000000000000000001;
export const SyncBatchedLane: Lane = 0b0000000000000000000000000000010;
export const InputDiscreteHydrationLane: Lane = 0b0000000000000000000000000000100;
const InputDiscreteLanes: Lanes = 0b0000000000000000000000000011000;
const InputContinuousHydrationLane: Lane =0b0000000000000000000000000100000;
const InputContinuousLanes: Lanes = 0b0000000000000000000000011000000;
export const DefaultHydrationLane: Lane = 0b0000000000000000000000100000000;
export const DefaultLanes: Lanes = 0b0000000000000000000111000000000;
const TransitionHydrationLane: Lane = 0b0000000000000000001000000000000;
const TransitionLanes: Lanes = 0b0000000001111111110000000000000;
const RetryLanes: Lanes = 0b0000011110000000000000000000000;
export const SomeRetryLane: Lanes = 0b0000010000000000000000000000000;
export const SelectiveHydrationLane: Lane = 0b0000100000000000000000000000000;
const NonIdleLanes = 0b0000111111111111111111111111111;
export const IdleHydrationLane: Lane = 0b0001000000000000000000000000000;
const IdleLanes: Lanes = 0b0110000000000000000000000000000;
export const OffscreenLane: Lane = 0b1000000000000000000000000000000;
2.2调度优先级
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
2.3优先级等级
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;
export const NoPriority: ReactPriorityLevel = 90;
LanePriority
与SchedulerPriority
通过一下方式进行转换
// 把 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.');
}
}
3.位运算符
上面说到了lane采用了位掩码特性,在了解“位掩码”之前,首先要学会位运算符。
按位非运算符 ~
:
对每一位执行非 操作,也可以理解为取反码
A = 0b1101
~A = 0b0010
按位与运算符 &
对每一位执行与(AND) 操作,只要对应位置均为 1 时,结果才为 1,否则为 0。
A = 0b1101
B = 0b0101
A & B = 0b0101
按位或运算符 |
对每一位执行或(OR) 操作,只要对应位置有一个 1 时,结果就为 1。
A = 0b1101
B = 0b0101
A | B = 0b1101
按位异或运算符 ^
对每一位执行异或(XOR) 操作,当对应位置有且只有一个 1 时,结果就为 1,否则为 0。
A = 0b1101
B = 0b0101
A | B = 0b1000
学完这些基本概念,我们就来看看优先级的设计机制是什么样的。
4.优先级机制如何设计
- 首先我们会维护一个队列,可以存储所有被占用的赛道。就要涉及到合并赛道的概念。
- 其次,我们会根据优先级关系从队列中找到最高优先级的赛道。
- 找到优先级赛道后我们还需要释放这个赛道。
4.1 合并赛道
场景:比如说现在调度的任务优先级为DefaultLane
,此时用户通过点击触发了更新,有一个更高的优先级SyncLane
运算核心:按位或运算符 |
运算过程:
假设DefaultLane优先级为0b10000,SyncLane优先级为0b00001
我们通过位或运算 0b10000 | 0b00001 得到 0b10001,两个优先级分别占用了第一条和第五条赛道。
0b10000 |
0b00001 =
0b10001
4.2 找出最高优先级赛道
场景:当前有 DefaultLane
和 SyncLane
两个优先级的任务占用赛道,我需要先调度优先级最高的任务,所以需要找出当前优先级最高的赛道
运算核心:位与运算符&
和取反运算符-
运算过程:
假设DefaultLane优先级为0b10000,SyncLane优先级为0b00001, 队列为0b10001
我们通过位与运算符和取反运算符进行操作 0b10001 & -0b10001 = 0b00001,得到0b00001,对应的就是我们SyncLane
-0b10001 = 0b00001 (-0b10001取反为0b00001)
0b00001 &
0b10001 =
0b00001
4.3 释放赛道
场景:当找出的最高优先级SyncLane
执行完毕后,我们需要释放这个赛道。
运算核心:位与运算符&
和位与运算符~
运算过程:
假设DefaultLane优先级为0b10000,SyncLane优先级为0b00001, 队列为0b10001
我们通过位非运算符和位与运算符进行操作 0b10001 & ~0b00001 = 0b10000,得到0b10000,对应的就是我们DefaultLane
~0b00001 = 0b11110
0b11110 &
0b10001 =
0b10000
5.React运行时的优先级机制
当我们调用hook
的时候,主要工作为以下4步:
- 获取本次更新的优先级
- 创建
Update
对象 - 将本次更新优先级关联到当前Fiber节点、父级节点和应用根节点
- 发起
ensureRootIsScheduled
调度
我们重点来讲解一下最后一步,究竟是如何进行调度的?
主要流程分为以下三步:
- 中断低优先级任务
- 任务队列移出优先级任务
- 低优先级任务重启
中断低优先级任务
var existingCallbackNode = root.callbackNode; // 当前 render 阶段正在进行的任务
var existingCallbackPriority = root.callbackPriority; // 当前 render 阶段正在进行的任务优先级
var newCallbackPriority = getHighestPriorityLane(nextLanes); // 此次调度优先级
if (existingCallbackPriority === newCallbackPriority) { // 判断优先级是否相等,相等就合并任务
...
return;
}
if (existingCallbackNode != null) {
cancelCallback(existingCallbackNode);
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
这里会判断当前正在进行的任务和此次调度的任务的优先级是否相等,如果相等,就合并到当前正在进行的任务。如果不相等,代表此次更新任务的优先级更高,需要打断当前正在进行的任务。如何打断任务呢?cancelCallback
就是用来中断任务的,其实就是执行currentTask.callback = null
任务队列移出任务
当我们需要中断任务后,在workLoop
事件循环中,我们需要将这个任务移除队列中,调用的就是pop(taskQueue)
} else {
// 如果任务被取消(这时currentTask.callback = null), 将其移出队列
pop(taskQueue);
}
低优先级任务重启
上一步中说道一个低优先级任务从 taskQueue
中被移出。那高优先级任务执行完毕之后,如何重启回之前的低优先级任务呢?
回到我们赛道的概念,当高优先级任务执行完毕之后,会释放赛道。那么未执行的任务任然还在赛道上,所以此时我们在重新调用一下ensureRootIsScheduled
发起一次新的调度,去重启低优先级任务的执行
// 确定下一个要工作的通道,以及它们的优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
);
// 如果 nextLanes 为 NoLanes,就证明所有任务都执行完毕了
if (nextLanes === NoLanes) {
// Special case: There's nothing to work on.
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
}
root.callbackNode = null;
root.callbackPriority = NoLane;
return;
}
// 此时代表赛道还有任务未执行完,就是那些被打断的低优先级任务
6.饥饿任务问题
通过上面我们知道,被打断的任务可以等到高优先级任务执行完毕后,再重新调用ensureRootIsScheduled
就可以继续执行了。那么,如果一直有高优先级任务进来,那么这些低优先级任务岂不是永远执行不了?
所以 react 为了处理解决饥饿任务问题,react 在 ensureRootIsScheduled 函数开始的时候做了以下处理:
let lanes = pendingLanes & ~RetryLanes;
while (lanes > 0) {
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
// Found a pending lane with no expiration time. If it's not suspended, or
// if it's pinged, assume it's CPU-bound. Compute a new expiration time
// using the current time.
if (
(lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {
// Assumes timestamps are monotonically increasing.
expirationTimes[index] = computeExpirationTime(lane, currentTime);
}
} else if (expirationTime <= currentTime) {
// This lane expired
root.expiredLanes |= lane;
}
lanes &= ~lane;
}
- 遍历每条赛道,判断每条赛道的过期时间是否为
NoTimestamp
,如果是,且该赛道存在待执行的任务,则为该赛道初始化过期时间 - 如果该赛道已存在过期时间,且过期时间已经小于当前时间,则代表任务已过期,需要将当前优先级合并到
expiredLanes
,这样在下一轮 render 阶段就会以同步优先级调度了。
站在巨人的肩膀上
总结:
1.数据更新:更新数据时,生成一个Update
对象,对象里面都有一个 lane
属性,代表此次更新的优先级。
2.高优先级打断低优先级: 会判断当前正在进行的任务和此次任务的优先级对比,如果相同,合并任务执行。如果不相同,就表示有高优先级任务过来,需要打断掉正在执行的任务,并且次任务从任务列表里移出。
3.重启低优先级任务:当高优先级任务执行完毕后,会重新进行一次ensureRootIsScheduled
调度。重启之前被打断的低优先级任务。
4.饥饿任务唤醒: ensureRootIsScheduled
函数开始时会对每个赛道设置一个过期时间,当任务过期时,下一次就会依同步任务执行,不会被打断。