expirationTime与优先级
# 目录
# 调度优先级
react 中优先级分为四种:
事件优先级:按照用户事件的交互紧急程度,划分的优先级
更新优先级:事件导致React产生的更新对象(update)的优先级(update.lane)
任务优先级:产生更新对象之后,React去执行一个更新任务,这个任务所持有的优先级
调度优先级:Scheduler依据React更新任务生成一个调度任务,这个调度任务所持有的优先级
2
3
4
参考:React 中的优先级 (opens new window)
这里我们探讨的是调度优先级。在上文中我们已经知道虽然 js 是单线程执行的,但是现代的浏览器可以通过 requestIdleCallback
和 requestAnimationFrame
来执行不同优先级的任务。通过这种优先级的管理,可以让页面的渲染更加流畅,而不至于让低优先级的任务阻塞了高优先级的任务的执行。
react 中配合浏览器来实现优先级管理的正是前文所述的 fiber 系统,只是前文我们主要在研究 fiber 在渲染中的创建和更新流程,现在我们就来着重分析 fiber 系统对于优先级管理所发挥的重要角色。这里我们只分析优先级的管理,至于不同优先级的任务是如何具体执行的,我们将在渲染器中具体分析。
# priorityLevel
在使用 expirationTime 之前 react 内部对优先级进行了划分,针对不同的优先级来进行调度。
// 优先级的分类,依次变高
export const NoWork = 0;
// TODO: Think of a better name for Never. The key difference with Idle is that
// Never work can be committed in an inconsistent state without tearing the UI.
export const Never = 1;
// Idle is slightly higher priority than Never. It must completely finish in order to be consistent.
export const Idle = 2;
export const Batched = Sync - 1;
export const Sync = MAX_SIGNED_31_BIT_INT; // Max int32: Math.pow(2, 31) - 1
2
3
4
5
6
7
8
9
- NoWork: 最低优先级,没有需要处理的任务
- Never:优先级低于 Idle,不阻塞 UI 渲染
- Idle:异步执行,不阻塞 UI 渲染
- Sync:最高优先级,立即执行(同步执行)
不同优先级的任务对应的执行时机不同,请看 inferPriorityFromExpirationTime 函数:
export const HIGH_PRIORITY_EXPIRATION = 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
export function inferPriorityFromExpirationTime(
currentTime: ExpirationTime,
expirationTime: ExpirationTime,
): ReactPriorityLevel {
if (expirationTime === Sync) {
return ImmediatePriority;
}
if (expirationTime === Never || expirationTime === Idle) {
return IdlePriority;
}
const msUntil =
expirationTimeToMs(expirationTime) - expirationTimeToMs(currentTime);
if (msUntil <= 0) {
return ImmediatePriority;
}
if (msUntil <= HIGH_PRIORITY_EXPIRATION + HIGH_PRIORITY_BATCH_SIZE) {
return UserBlockingPriority;
}
if (msUntil <= LOW_PRIORITY_EXPIRATION + LOW_PRIORITY_BATCH_SIZE) {
return NormalPriority;
}
// TODO: Handle LowPriority
// Assume anything lower has idle priority
return IdlePriority;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
可以看到这里的真正的任务优先级包括:
ImmediatePriority, // 同步立即执行
UserBlockingPriority, // 高优先级任务,阻塞 UI 任务
NormalPriority, // 普通优先级任务
IdlePriority, // 异步延期执行
2
3
4
这里仅仅是从 priorityLevel 机制到 expirationTime 机制的过度。在新版本中对此作了调整,仅做理解即可。
# expirationTime
react 中的调度优先级是通过 expirationTime
来实现的(暂不考虑新版本中的 lanes)。 expirationTime
字面意思是 “到期时间” 或者 “过期时间”,指的是距离任务被执行还需要等待的时间,到期时间越短,说明优先级越高。具体来理解,当调度任务由调度器接手时会根据优先级给这个任务分配一个到期时间,当到期时间达到时,当前任务就会被回调,进入调和器去调度执行。
先来看下 expirationTime 的定义,在 react-reconciler 包中 ReactFiberExpirationTime.js 文件:
export type ExpirationTime = number;
expirationTime 是 number 类型,通过比较 expirationTime 和 currentTime 可以将计算出 expirationTime 的值。
我们先来看看 expirationTime 和时间单位(ms)是怎么换算的?
export const Sync = MAX_SIGNED_31_BIT_INT;
export const Batched = Sync - 1;
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = Batched - 1;
export function msToExpirationTime(ms: number): ExpirationTime {
// Always add an offset so that we don't clash with the magic number for NoWork.
// 这里使用 MAGIC_NUMBER_OFFSET 是为了避免让这个值等于 noWork。
return MAGIC_NUMBER_OFFSET - ((ms / UNIT_SIZE) | 0);
}
export function expirationTimeToMs(expirationTime: ExpirationTime): number {
return (MAGIC_NUMBER_OFFSET - expirationTime) * UNIT_SIZE;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
这里是 expirationTime 和毫秒的换算公式:
|0
是取整的操作。比如:2.3|0=2
,'333.4'|0=333
,'e333'|0=0
。- 这里我们把常量带进去整理下:
- msToExpirationTime:1073741821-((ms/10)|0)。可以考到这是一个减函数,当 ms 很小时,expirationTime 将会很大。
- msUntil:((currentTime/10)|0)-((expirationTime/10)|0)。可以看到 msUntil 的值如果是负值,则应该立即执行,msUntil 值越小优先级越高。
- expirationTimeToMs:(1073741821-expirationTime)*10。
下面我们来看下 fiber 机制中 expirationTime 是如何计算的:
# computeExpirationForFiber
这个函数会为 fiber 计算 expirationTime,根据调度器给出的优先级,计算 expirationTime。
function computeExpirationForFiber(
currentTime: ExpirationTime,
fiber: Fiber,
suspenseConfig: null | SuspenseConfig,
): ExpirationTime {
const mode = fiber.mode;
if ((mode & BatchedMode) === NoMode) {
// 当 mode 不是 BatchedMode 时,同步渲染
return Sync; // 1073741823 MAX_SIGNED_31_BIT_INT
}
// 从调度器获得优先级
const priorityLevel = getCurrentPriorityLevel();
if ((mode & ConcurrentMode) === NoMode) {
// 不是 ConcurrentMode
return priorityLevel === ImmediatePriority ? Sync : Batched;
}
if ((executionContext & RenderContext) !== NoContext) {
// executionContext 为 RenderContext
// Use whatever time we're already rendering
// TODO: Should there be a way to opt out, like with `runWithPriority`?
return renderExpirationTime; // NoWork 0
}
let expirationTime;
if (suspenseConfig !== null) {
// Compute an expiration time based on the Suspense timeout.
expirationTime = computeSuspenseExpiration(
currentTime,
suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
);
} else {
// Compute an expiration time based on the Scheduler priority.
switch (priorityLevel) {
case ImmediatePriority:
expirationTime = Sync;
break;
case UserBlockingPriority:
// TODO: Rename this to computeUserBlockingExpiration
expirationTime = computeInteractiveExpiration(currentTime);
break;
case NormalPriority:
case LowPriority: // TODO: Handle LowPriority
// TODO: Rename this to... something better.
expirationTime = computeAsyncExpiration(currentTime);
break;
case IdlePriority:
expirationTime = Idle;
break;
default:
invariant(false, 'Expected a valid priority level');
}
}
// If we're in the middle of rendering a tree, do not update at the same
// expiration time that is already rendering.
// TODO: We shouldn't have to do this if the update is on a different root.
// Refactor computeExpirationForFiber + scheduleUpdate so we have access to
// the root when we check for this condition.
// 如果 FiberTree 已经在渲染了,不用重复更新超时时间,减 1 是为了区别当前的 batch
if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
// This is a trick to move this update into a separate batch
expirationTime -= 1;
}
return expirationTime;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
- UserBlockingPriority、NormalPriority 和 LowPriority 这三种优先级应为异步执行,分别由
computeInteractiveExpiration
和computeAsyncExpiration
两个函数来计算 expirationTime。 - computeInteractiveExpiration 的优先级比 computeAsyncExpiration 要高。
- react fiber 中的 mode:
export const NoMode = 0b0000; // 0
export const StrictMode = 0b0001; // 1
export const BatchedMode = 0b0010; // 2
export const ConcurrentMode = 0b0100; // 4
export const ProfileMode = 0b1000; // 8
2
3
4
5
- StrictMode 严格模式:检测废弃 API,React16-17 开发环境使用。
- BatchedMode 普通模式:同步渲染,React15-16 的生产环境用。
- ConcurrentMode 并发模式:异步渲染,React17 的生产环境用。
- ProfileMode 性能测试模式:检测性能问题,React16-17 开发环境使用。
- fiber mode 中的位运算技巧。
在 2 的 n 次方序列中(不包括 0),任何数与自己相与值仍然是自己,与其他数相与值为 0。即 (x&x)===x,(x&y)===0。 因此我们现在需要将事物进行分类,如流程、种类等,可以用到这个技巧。
# computeExpirationBucket
export const HIGH_PRIORITY_EXPIRATION = __DEV__ ? 500 : 150;
export const HIGH_PRIORITY_BATCH_SIZE = 100;
function computeInteractiveExpiration(currentTime: ExpirationTime) {
return computeExpirationBucket(
currentTime,
HIGH_PRIORITY_EXPIRATION,
HIGH_PRIORITY_BATCH_SIZE,
);
}
export const LOW_PRIORITY_EXPIRATION = 5000;
export const LOW_PRIORITY_BATCH_SIZE = 250;
export function computeAsyncExpiration(
currentTime: ExpirationTime,
): ExpirationTime {
return computeExpirationBucket(
currentTime,
LOW_PRIORITY_EXPIRATION,
LOW_PRIORITY_BATCH_SIZE,
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
这两个函数内部都是调用 computeExpirationBucket 来计算超时时间的。现在我们着重来看看 computeExpirationBucket
这个函数:
const UNIT_SIZE = 10;
const MAGIC_NUMBER_OFFSET = 2;
function ceiling(num: number, precision: number): number {
return (((num / precision) | 0) + 1) * precision;
}
function computeExpirationBucket(
currentTime,
expirationInMs,
bucketSizeMs,
): ExpirationTime {
return (
MAGIC_NUMBER_OFFSET +
ceiling(
currentTime - MAGIC_NUMBER_OFFSET + expirationInMs / UNIT_SIZE,
bucketSizeMs / UNIT_SIZE,
)
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
将常量带入,可以得到:((((currentTime - 2 + 5000 / 10) / 25) | 0) + 1) * 25
((((26 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 525
((((27 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 550
((((51 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 550
((((52 - 2 + 5000 / 10) / 25) | 0) + 1) * 25 575
2
3
4
可以看到:在 27-51 这段的 currentTime 里,对应的 expirationTime 都是 550。同样的 expirationTime 也就意味着这些任务将会在同一时间被调度器执行回调,也就是说这些任务会同时去做更新,这就是 react 中的批量更新。
低优先的过期时间间隔是 25ms(UserBlockingPriority),高优先级的过期时间间隔是 10ms(NormalPriority、LowPriority)。
批量更新允许 react 将优先级差不多的一批更新批量的一起更新,这样就可以避免频繁的状态变化曹成频繁更新,导致一些没有意义的中间状态也被执行更新的问题。这样的更新机制极大地提高了 react 的更新效率。
# 小结
我编文章讲到 fiber expiration 优先级机制,以及在异步更新的 fiber 中 expiration 的计算方法批量更新特性。