概述
在React15的时候,React使用的是从根节点往下递归
的方式同步创建虚拟Dom,由于递归具有同步不可中断的特性,所以当执行长任务时(通常以60帧为标准,即16.6ms)就会长时间占用主线程长时间无响应,导致页面卡顿,对于交互及其不友好。所以React16中新增了fiber架构和scheduler调度(之前只有渲染器Renderer、协调器Reconciler),在该版本中新增了时间分片逻辑,将一个长任务切分为多个小任务,并在浏览器空闲时,根据优先级执行。其中时间分片的前提就是中断和恢复。本文以[email protected]来解释React中的时间分片以及中断和恢复原理。
调度器Scheduler
在了解时间分片之前,先了解下调度器是什么?
所谓Scheduler,如名所示就是用于任务调度,判断在什么时候执行任务避免长时间阻塞主线程的一个工具包。React官方将其设置为独立的包,不仅仅用于React,当其他项目需要对长任务进行优化调度时都可以使用该包,所以其命名为单独的scheduler
,而不是react-scheduler
。
其主要有两个特性:
- 优先级,根据优先级高低决定执行哪个任务,优先执行高优先级任务。
- 时间分片,其内部设置了5ms的执行时间,当任务执行超过设置的时间则会中断执行,并将主线程交回浏览器,避免长时间无响应导致卡顿,在浏览器空闲时再通过恢复来继续执行中断的任务。
针对上面的两个特性,我们来一一解释:
优先级
在React内部优先级有三种:Event优先级
、Lane优先级
、Scheduler优先级
Event优先级:事件优先级,React会将原生事件封装成合成事件时,会根据事件类型携带不同的事件优先级。Event优先级定义时就映射了Lane优先级,即使用Lane优先级来进行的赋值。
// react/packages/react-reconciler/src/ReactEventPriorities.js
export const NoEventPriority: EventPriority = NoLane; // 无优先级
export const DiscreteEventPriority: EventPriority = SyncLane; // 离散优先级
export const ContinuousEventPriority: EventPriority = InputContinuousLane; // 连续输入优先级
export const DefaultEventPriority: EventPriority = DefaultLane; // 默认优先级
export const IdleEventPriority: EventPriority = IdleLane; // 空闲优先级
Lane优先级:车道优先级,位于react-reconciler
协调器中,用于规定更新任务的执行顺序。
为什么是车道优先级呢? 可以看定义该优先级是使用二进制定义的,一方面二进制运算性能是很好的,其次React采用的是
位优先级
,不同的位表示具有该优先级,然后通过位运算能快速获取优先级。然后看后面的二进制定义,是不是像车道的样子,所以命名为车道优先级也表示其是使用二进制定义的。
// react/packages/react-reconciler/src/ReactFiberLane.js
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncHydrationLane: Lane = /* */ 0b0000000000000000000000000000001;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000010;
export const SyncLaneIndex: number = 1;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000100;
export const InputContinuousLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000010000;
export const DefaultLane: Lane = /*
Scheduler优先级:调度优先级,位于scheduler
中,用于制定更新任务调度的执行顺序。
// react/packages/scheduler/src/SchedulerPriorities.js
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
// TODO: Use symbols?
export const NoPriority = 0; // 无优先级
export const ImmediatePriority = 1; // 离散事件,点击、keydown、input这种单个事件
export const UserBlockingPriority = 2; // 连续输入优先级,滚动、拖拽等连续事件
export const NormalPriority = 3; // 普通优先级(默认优先级)
export const LowPriority = 4; // 低优先级
export const IdlePriority = 5; // 空闲优先级
并且每种调度优先级都有自己的一个timeout,在unstable_scheduleCallback
创建任务时会根据这个时延来设置expirationTime
,并进一步设置sortIndex
。这里只是介绍下,知道有这个东西,下面会详细说明。
// react/packages/scheduler/src/SchedulerFeatureFlags.js
export const enableSchedulerDebugging = false;
export const enableProfiling = false;
export const frameYieldMs = 5;
export const userBlockingPriorityTimeout = 250;
export const normalPriorityTimeout = 5000;
export const lowPriorityTimeout = 10000;
转换体系:Scheduler是独立的一个包,所以自己内部维护了一个优先级(Scheduler优先级
),所以需要进行优先级转换:Lane优先级 -> Event优先级 -> Scheduler
优先级进行Lane优先级和Scheduler优先级的映射。比如当我们点击按钮触发onclick
事件时,由于合成事件携带了Event优先级,并且该Event优先级是Lane优先级的映射,然后在协调器中创建任务调度时,会根据该Event优先级转换为Scheduler优先级。如下所示:在下面代码中创建调度任务时会执行scheduleTaskForRootDuringMicrotask
函数,并在其中会根据lane优先级获取Event优先级,然后转换为Scheduler优先级
// react/packages/react-reconciler/src/ReactEventPriorities.js
export function lanesToEventPriority(lanes: Lanes): EventPriority {
const lane = getHighestPriorityLane(lanes);
if (!isHigherEventPriority(DiscreteEventPriority, lane)) {
return DiscreteEventPriority;
}
if (!isHigherEventPriority(ContinuousEventPriority, lane)) {
return ContinuousEventPriority;
}
if (includesNonIdleWork(lane)) {
return DefaultEventPriority;
}
return IdleEventPriority;
}
// react/packages/react-reconciler/src/ReactFiberRootScheduler.js
import {
ImmediatePriority as ImmediateSchedulerPriority,
UserBlockingPriority as UserBlockingSchedulerPriority,
NormalPriority as NormalSchedulerPriority,
IdlePriority as IdleSchedulerPriority,
cancelCallback as Scheduler_cancelCallback,
scheduleCallback as Scheduler_scheduleCallback,
now,
} from './Scheduler';
function scheduleTaskForRootDuringMicrotask(
root: FiberRoot,
currentTime: number,
): Lane {
// ...
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority