前面的文章中说到了在协调之前还有一个调度的步骤,不过这个调度在目前使用 ReactDOM.render
这个 API
的情况下和过去的版本并无区别,React v17
是个过渡版本。
异步可中断模式(Concurrent Mode
)下,调度器(Scheduler
)才能发挥出其威力,React v18
才会正式支持此模式。
什么是调度?为什么需要调度?
一般来说主流浏览器的刷新频率为 60 Hz,即约每 16.6 ms (1000 ms / 60 Hz) 浏览器就会刷新一次,小于了这个帧率就会让人感到卡顿。
浏览器是单线程的,所以 JS
线程和渲染线程并不能同时执行。
如果代码执行时间比较长(超过了 16.6 ms),那么某一帧或者某几帧就会因为代码执行出现没有时间进行渲染的情况,这时就会出现掉帧的情况,那么就会感受到卡顿。在 React V15
及之前的版本使用的是非 Fiber
的架构,所有 DOM
节点的遍历是用的递归的方式,无法中断又恢复继续执行,所以代码都是同步执行的,就导致了卡顿的问题的存在。
在 React V16
之后使用了 Fiber
架构,可以将执行时间比较长的任务碎片化。每一个 DOM
节点都对应一个 Fiber
,而他们又通过链表的方式连起来,这就可以把遍历方式改成链表深度遍历,记录当前指针就可以实现遍历的暂停与恢复。
如果一个任务耗时比较长一帧执行不为,那么在每一帧就执行一小段任务,然后中断,都留下一些时间来让渲染进程正常执行,下一帧再花点时间来继续执行任务,如此循环往复直到任务执行完。这样每一帧的渲染都没有被阻塞,我们也就不会感受到卡顿了,而这个将任务碎片化,执行,暂停,又执行的过程就叫调度。
Scheduler
调度当然需要调度器,Scheduler
就是当我们配合时间切片,能根据宿主环境性能,为每个工作单元分配一个可运行时间,实现“异步可中断的更新”的调度器。
Scheduler
有两个功能:
- 时间切片
- 优先级调度
时间切片
除了浏览器重排/重绘,浏览器一帧中可以用于执行 JS
的时机依次是:
宏任务 -> 微任务 -> requestAnimationFrame -> 浏览器重排/重绘 -> requestIdleCallback
requestAnimationFrame 在“浏览器重排/重绘”前执行 JS
,这是浏览器渲染之前的最后时机
requestIdleCallback 在“浏览器重排/重绘”后如果当前帧还有空余时间时将被调用
所以时间切片的本质就是模拟实现 requestIdleCallback
。
为什么要模拟实现呢,因为requestIdleCallback
在浏览器的兼容性上其实是不太友好的,MDN 上也标注其为一个实验中的功能。
事实上 React
是使用宏任务模拟实现 requestIdleCallback
的,如果宿主环境支持 MessageChannel
就会使用这个 API
,否则会直接使用 setTimeout
来实现。
优先级调度
首先这里的优先级,并不是下面要讲的 Lane
模型的那种优先级,Scheduler
本身也是独立于 React
的包,所以他的优先级也是独立于 React
的优先级的。
Scheduler
的优先级有五种,也分别对应了五个不同的过期时间,任务一旦过期就需要立即执行掉。
大型的 React
项目中可能同时存在很多不同优先级的任务,有些优先级高会被立即执行,有些优先级低会被延时执行,两种任务会被分到不同的队列(timerQueue
保存延时的任务,taskQueue
保存立即执行的任务)。
每当有新的被延时的任务被注册,timerQueue
就会进行重排。
当 timerQueue
中的任务时间到了我们就将其取出并加入 taskQueue
。
最后是取出 taskQueue
中最早过去的任务并执行他。
为了能在两个队列中快速地找到最早需要被执行的那个任务,Scheduler
实现了一个小顶堆(SchedulerMinHeap
)。我们知道小顶堆的特点是:每个节点的值都小于等于子树中每个节点值,也就是说堆顶就是最小值,对应 Scheduler
中就是堆顶节点就是最早需要被执行的任务,取出他的时间复杂度为 O(1)
。
源码
scheduleCallback 如何被调用
在前面讲协调的文章里面,讲了 ReactDOM.render
这个 API
的调用过程,不过讲调度器还是得用 ReactDOM.createRoot
这个 API
以使用 Concurrent Mode
。所以其调用过程略有不同。其调用过程大致如下图所示:
可以看到最后是调了 schedulerCallback
,而且 performConcurrentWorkOnRoot
也是被 这其实就是 Scheduler
中的 unstable_scheduleCallback
,unstable
代表现在还不稳定,还在开发中,不是正式版本中的功能(前面也说过要等 React V18 正式版才能用上稳定版的 Concurrent Mode)。
下面看一下 Scheduler
的代码,执行过程见注释:
// path: /packages/scheduler/src/forks/Scheduler.js
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前时间
var currentTime = getCurrentTime();
// 生成开始时间,配置了 delay 就加上延时
var startTime;
if (typeof options === 'object' && options !== null) {
var delay = options.delay;
if (typeof delay === 'number' && delay > 0) {
startTime = currentTime + delay;
} else {
startTime = currentTime;
}
} else {
startTime = currentTime;
}
var timeout;
// 5种优先级以及其对应的5种延时时间,根据优先级配置超时时间
switch (priorityLevel) {
case ImmediatePriority:
timeout = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 设置过期时间
var expirationTime = startTime + timeout;
// 配置新任务
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
// 开始时间大于当前时间,说明是一个延时任务,还不用开始执行,直接加入 timerQueue
if (startTime > currentTime) {
newTask.sortIndex = startTime;
push(timerQueue, newTask);
// taskQueue 为空,timerQueue 顶元素就是当前任务,说明所有的任务中,当前任务是最早的
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 开始时间小于等于当前时间,说明已到开始时间,直接加入 taskQueue
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
// 挂起宏任务,执行渲染任务
requestHostCallback(flushWork);
}
}
return newTask;
}
requestHostCallback
是一个比较关键的调用,这个函数调用的时候会根据宿主环境的具体情况选择由哪个 API
来执行宏任务,源码中可以看到其优先级顺序为 setImmediate
> MessageChannel
> setTimeout
。为什么是这个顺序呢,因为其执行的延时是逐渐下降的。
setTimeout(fn, 0)
其实是有 4ms 的延时的,setImmediate
虽然是同步任务执行完立马就会执行,但是只有 IE
和 Node.JS
才支持,像 Chrome
都不支持,而 MessageChannel
在 Chrome
等浏览器上支持,但是在 IE
上又不支持。
flushWork
的调用过程比较复杂,可以进一步看一下 workLoop
、performConcurrentWorkOnRoot
函数中的逻辑,总结起来就是不断地取出 taskQueue
中最早过期的任务进行执行,恢复中断继续执行也是在这里去实现的。
performUnitOfWork 中断标志
Concurrent Mode
下,协调阶段始于 performConcurrentWorkOnRoot
, 然后会调到 renderRootConcurrent
-> workLoopConcurrent
函数。
// path: /packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function workLoopConcurrent() {
// Perform work until Scheduler asks us to yield
while (workInProgress !== null && !shouldYield()) {
performUnitOfWork(workInProgress);
}
}
shouldYield()
如果为 true
那么循环将会被中断。接下来看一下 shouldYield
是什么。
首先是调到了下面这个地方。
// path: /packages/react-reconciler/src/Scheduler.js
import * as Scheduler from 'scheduler';
export const scheduleCallback = Scheduler.unstable_scheduleCallback;
export const cancelCallback = Scheduler.unstable_cancelCallback;
export const shouldYield = Scheduler.unstable_shouldYield;
export const requestPaint = Scheduler.unstable_requestPaint;
export const now = Scheduler.unstable_now;
export const getCurrentPriorityLevel =
Scheduler.unstable_getCurrentPriorityLevel;
export const ImmediatePriority = Scheduler.unstable_ImmediatePriority;
export const UserBlockingPriority = Scheduler.unstable_UserBlockingPriority;
export const NormalPriority = Scheduler.unstable_NormalPriority;
export const LowPriority = Scheduler.unstable_LowPriority;
export const IdlePriority = Scheduler.unstable_IdlePriority;
export type SchedulerCallback = (isSync: boolean) => SchedulerCallback | null;
顺藤摸瓜 shouldYield
-> unstable_shouldYield
-> shouldYieldToHost
。
// path: /packages/scheduler/src/forks/Scheduler.js
function shouldYieldToHost() {
if (
enableIsInputPending &&
navigator !== undefined &&
navigator.scheduling !== undefined &&
navigator.scheduling.isInputPending !== undefined
) {
const scheduling = navigator.scheduling;
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
if (needsPaint || scheduling.isInputPending()) {
return true;
}
const timeElapsed = currentTime - (deadline - yieldInterval);
return timeElapsed >= maxYieldInterval;
} else {
return false;
}
} else {
return getCurrentTime() >= deadline;
}
}
shouldYieldToHost
函数做的主要事情就是调用 getCurrentTime
函数,获取 currentTime
,并和 deadline
对比。当前时间如果小于截止时间就返回 false
, 继续 performUnitOfWork
循环执行;如果当前时间大于等于截止时间,那么就需要对比这两个时间的差值以及 yieldInterval
时间来决定是否需要打断 performUnitOfWork
的执行。yieldInterval
默认为 5 ms,会根据实际的宿主环境进行调整,见 Scheduler.js
的 forceFrameRate
函数,目前 unstable
版本中并未被外部调用。
所以 performUnitOfWork
能否执行全靠 Scheduler
调度,听他指挥。
优先级
优先级相关定义的代码如下:
// path: /packages/scheduler/src/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;
// path: /packages/scheduler/src/forks/Scheduler.js
var maxSigned31BitInt = 1073741823;
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
在之前讲渲染的时候讲过,在 commit
阶段的 before Mutation
阶段,我们执行了 useEffect
:
// 路径:packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function commitRootImpl(root, renderPriorityLevel) {
// 省略一些代码
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
// 异步调用 useEffect
scheduleCallback(NormalSchedulerPriority, () => {
flushPassiveEffects();
return null;
});
}
// 省略一些代码
}
这里的回调便是通过 scheduleCallback
调度的,优先级为 NormalSchedulerPriority
,即NormalPriority
。最多可被延时 5000ms 执行,这就是为什么我们在这么前面就调用的 useEffect
,但是其执行时间却很晚的原因。
Lane 模型
Lane
模型是 React
的优先级系统,是一个控制不同优先级之间的关系与行为的策略。
Lane
原意为赛道,不同的赛车行驶在不同的赛道上,内圈赛道短,外圈赛道长,某几个临近的赛道长度可以看作是一样的。
Lane
模型借鉴了同样的概念,使用 31 为的二进制表示 31 条赛道,位数越小(也就是越靠右,越靠内圈)的赛道优先级越高,某些相邻的赛道拥有相同的优先级。
在开启 Concurrent Mode
的情况下:过期任务或者同步任务的优先级是最高的,使用 SyncLane
赛道;用户交互产生的更新(比如:点击事件)使用较高优先级的赛道;网络请求产生的更新使用一般优先级的赛道;Suspense
使用低优先级的赛道。
源码
赛道定义
SyncLane
赛道优先级最高,优先级逐一降低,OffscreenLane
赛道优先级最低。
// path: packages/react-reconciler/src/ReactFiberLane.new.js
export const TotalLanes = 31;
export const NoLanes: Lanes = /* */ 0b0000000000000000000000000000000;
export const NoLane: Lane = /* */ 0b0000000000000000000000000000000;
export const SyncLane: Lane = /* */ 0b0000000000000000000000000000001;
export const InputContinuousHydrationLane: Lane = /* */ 0b0000000000000000000000000000010;
export const InputContinuousLane: Lanes = /* */ 0b0000000000000000000000000000100;
export const DefaultHydrationLane: Lane = /* */ 0b0000000000000000000000000001000;
export const DefaultLane: Lanes = /* */ 0b0000000000000000000000000010000;
const TransitionHydrationLane: Lane = /* */ 0b0000000000000000000000000100000;
const TransitionLanes: Lanes = /* */ 0b0000000001111111111111111000000;
// 省略一些代码
const RetryLanes: Lanes = /* */ 0b0000111110000000000000000000000;
// 省略一些代码
export const SomeRetryLane: Lane = RetryLane1;
export const SelectiveHydrationLane: Lane = /* */ 0b0001000000000000000000000000000;
const NonIdleLanes = /* */ 0b0001111111111111111111111111111;
export const IdleHydrationLane: Lane = /* */ 0b0010000000000000000000000000000;
export const IdleLane: Lanes = /* */ 0b0100000000000000000000000000000;
export const OffscreenLane: Lane = /* */ 0b1000000000000000000000000000000;
// 省略一些代码
赛道优先级是如何分配给不同事件的
在使用 createRoot
这个 API
去创建根节点的时候会调用 listenToAllSupportedEvents
去添加所有事件监听,最终会调到 getEventPriority
。
// path: packages/react-dom/src/events/ReactDOMEventListener.js
export function getEventPriority(domEventName: DOMEventName): * {
switch (domEventName) {
case 'cancel':
case 'click':
// 省略一些 case
case 'drop':
case 'focusin':
case 'focusout':
case 'input':
case 'invalid':
case 'keydown':
case 'keypress':
case 'keyup':
case 'mousedown':
case 'mouseup':
// 省略一些 case
return DiscreteEventPriority;
case 'drag':
case 'dragenter':
case 'dragexit':
case 'dragleave':
case 'dragover':
case 'mousemove':
// 省略一些 case
return ContinuousEventPriority;
case 'message': {
const schedulerPriority = getCurrentSchedulerPriorityLevel();
switch (schedulerPriority) {
case ImmediateSchedulerPriority:
return DiscreteEventPriority;
case UserBlockingSchedulerPriority:
return ContinuousEventPriority;
case NormalSchedulerPriority:
case LowSchedulerPriority:
return DefaultEventPriority;
case IdleSchedulerPriority:
return IdleEventPriority;
default:
return DefaultEventPriority;
}
}
default:
return DefaultEventPriority;
}
}
事件优先级定义:
// path: packages/react-reconciler/src/ReactEventPriorities.new.js
export const DiscreteEventPriority: EventPriority = SyncLane;
export const ContinuousEventPriority: EventPriority = InputContinuousLane;
export const DefaultEventPriority: EventPriority = DefaultLane;
export const IdleEventPriority: EventPriority = IdleLane;
结合事件优先级的定义可以看出事件的优先级被分成了四类,这四类优先级又对应着四类事件:像 click
、keydown
、mousedown
等离散事件都是优先级比较高的事件;drag
、mousemove
、scroll
之类的连续事件优先级次之。
接下来会根据获取到的事件的优先级分类,设置事件触发时拥有相对应优先级的回调函数。
// path: packages/react-dom/src/events/ReactDOMEventListener.js
export function createEventListenerWrapperWithPriority(
targetContainer: EventTarget,
domEventName: DOMEventName,
eventSystemFlags: EventSystemFlags,
): Function {
const eventPriority = getEventPriority(domEventName);
let listenerWrapper;
switch (eventPriority) {
case DiscreteEventPriority:
listenerWrapper = dispatchDiscreteEvent;
break;
case ContinuousEventPriority:
listenerWrapper = dispatchContinuousEvent;
break;
case DefaultEventPriority:
default:
listenerWrapper = dispatchEvent;
break;
}
return listenerWrapper.bind(
null,
domEventName,
eventSystemFlags,
targetContainer,
);
}
优先级相关计算
赛道优先级的计算都是二级制的位运算。
// path: packages/react-reconciler/src/ReactFiberLane.new.js
// 判断两个赛道是否存在交集
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {
return (a & b) !== NoLanes;
}
// 判断某个赛道是不是另一赛道的子集
export function isSubsetOfLanes(set: Lanes, subset: Lanes | Lane) {
return (set & subset) === subset;
}
// 赛道的或运算,合并俩赛道
export function mergeLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a | b;
}
// 从一个赛道中移除其某个子赛道
export function removeLanes(set: Lanes, subset: Lanes | Lane): Lanes {
return set & ~subset;
}
// 获取两个赛道的交集
export function intersectLanes(a: Lanes | Lane, b: Lanes | Lane): Lanes {
return a & b;
}
这些计算会在有更新的时候去判断这个更新的优先级到底够不够,不够的话就可能会被跳过并做一些优先级相关的其它计算,够的话就能正常执行。
React 如何使用 Lane 模型
当我们触发更新之后会调用 requestUpdateLane
函数,以获取当前的赛道,后面的逻辑会根据赛道的信息决定如何去执行。
// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
// 请求某个更新的赛道
export function requestUpdateLane(fiber: Fiber): Lane {
const mode = fiber.mode;
if ((mode & ConcurrentMode) === NoMode) {
return (SyncLane: Lane);
} else if () {
// 省略一些代码
}
// 省略一些代码
}
可以看出在非 ConcurrentMode
模式下,都会使用 SyncLane
赛道,也就是同步赛道。在 Concurrent
模式下,则会返回其对应的赛道。
然后会根据赛道的情况转换成对应的 Scheduler
的优先级,这样就能驱动 Scheduler
的调度了。
// path: packages/react-reconciler/src/ReactFiberWorkLoop.new.js
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// 省略一些代码
let newCallbackNode;
if (newCallbackPriority === SyncLane) {
// 省略一些代码
newCallbackNode = null;
} else {
let schedulerPriorityLevel;
switch (lanesToEventPriority(nextLanes)) {
case DiscreteEventPriority:
schedulerPriorityLevel = ImmediateSchedulerPriority;
break;
case ContinuousEventPriority:
schedulerPriorityLevel = UserBlockingSchedulerPriority;
break;
case DefaultEventPriority:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
case IdleEventPriority:
schedulerPriorityLevel = IdleSchedulerPriority;
break;
default:
schedulerPriorityLevel = NormalSchedulerPriority;
break;
}
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}