共计 9511 个字符,预计需要花费 24 分钟才能阅读完成。
react 源码解析 15.scheduler&Lane
视频解说(高效学习):进入学习
当咱们在相似上面的搜寻框组件进行搜寻时会发现,组件分为搜寻局部和搜寻后果展现列表,咱们冀望输入框能立即响应,后果列表能够有期待的工夫,如果后果列表数据量很大,在进行渲染的时候,咱们又输出了一些文字,因为用户输出事件的优先级是很高的,所以就要进行后果列表的渲染,这就引出了不同工作之间的优先级和调度
Scheduler
咱们晓得如果咱们的利用占用较长的 js 执行工夫,比方超过了设施一帧的工夫,那么设施的绘制就会出不的景象。
Scheduler 次要的性能是工夫切片和调度优先级,react 在比照差别的时候会占用肯定的 js 执行工夫,Scheduler 外部借助 MessageChannel 实现了在浏览器绘制之前指定一个工夫片,如果 react 在指定工夫内没比照完,Scheduler 就会强制交出执行权给浏览器
工夫切片
在浏览器的一帧中 js 的执行工夫如下
requestIdleCallback 是在浏览器重绘重排之后,如果还有闲暇就能够执行的机会,所以为了不影响重绘重排,能够在浏览器在 requestIdleCallback 中执行耗性能的计算,然而因为 requestIdleCallback 存在兼容和触发机会不稳固的问题,scheduler 中采纳 MessageChannel 来实现 requestIdleCallback,以后环境不反对 MessageChannel 就采纳 setTimeout。
在之前的介绍中咱们晓得在 performUnitOfWork 之后会执行 render 阶段和 commit 阶段,如果在浏览器的一帧中,cup 的计算还没实现,就会让出 js 执行权给浏览器,这个判断在 workLoopConcurrent 函数中,shouldYield 就是用来判断残余的工夫有没有用尽。在源码中每个工夫片时 5ms,这个值会依据设施的 fps 调整。
function workLoopConcurrent() {while (workInProgress !== null && !shouldYield()) {performUnitOfWork(workInProgress);
}
}
function forceFrameRate(fps) {// 计算工夫片
if (fps < 0 || fps > 125) {console['error'](
'forceFrameRate takes a positive int between 0 and 125,' +
'forcing frame rates higher than 125 fps is not supported',
);
return;
}
if (fps > 0) {yieldInterval = Math.floor(1000 / fps);
} else {yieldInterval = 5;// 工夫片默认 5ms}
}
工作的暂停
在 shouldYield 函数中有一段,所以能够晓得,如果以后工夫大于工作开始的工夫 +yieldInterval,就打断了工作的进行。
//deadline = currentTime + yieldInterval,deadline 是在 performWorkUntilDeadline 函数中计算出来的
if (currentTime >= deadline) {
//...
return true
}
调度优先级
在 Scheduler 中有两个函数能够创立具备优先级的工作
-
runWithPriority:以一个优先级执行 callback,如果是同步的工作,优先级就是 ImmediateSchedulerPriority
function unstable_runWithPriority(priorityLevel, eventHandler) {switch (priorityLevel) {// 5 种优先级 case ImmediatePriority: case UserBlockingPriority: case NormalPriority: case LowPriority: case IdlePriority: break; default: priorityLevel = NormalPriority; } var previousPriorityLevel = currentPriorityLevel;// 保留以后的优先级 currentPriorityLevel = priorityLevel;//priorityLevel 赋值给 currentPriorityLevel try {return eventHandler();// 回调函数 } finally {currentPriorityLevel = previousPriorityLevel;// 还原之前的优先级} }
-
scheduleCallback:以一个优先级注册 callback,在适当的机会执行,因为波及过期工夫的计算,所以 scheduleCallback 比 runWithPriority 的粒度更细。
- 在 scheduleCallback 中优先级意味着过期工夫,优先级越高 priorityLevel 就越小,过期工夫离以后工夫就越近,
var expirationTime = startTime + timeout;
例如 IMMEDIATE_PRIORITY_TIMEOUT=-1,那var expirationTime = startTime + (-1);
就小于以后工夫了,所以要立刻执行。 - scheduleCallback 调度的过程用到了小顶堆,所以咱们能够在 O(1)的复杂度找到优先级最高的 task,不理解能够查阅材料,在源码中小顶堆寄存着工作,每次 peek 都能取到离过期工夫最近的 task。
-
scheduleCallback 中,未过期工作 task 寄存在 timerQueue 中,过期工作寄存在 taskQueue 中。
新建 newTask 工作之后,判断 newTask 是否过期,没过期就退出 timerQueue 中,如果此时 taskQueue 中还没有过期工作,timerQueue 中离过期工夫最近的 task 正好是 newTask,则设置个定时器,到了过期工夫就退出 taskQueue 中。
当 timerQueue 中有工作,就取出最早过期的工作执行。
- 在 scheduleCallback 中优先级意味着过期工夫,优先级越高 priorityLevel 就越小,过期工夫离以后工夫就越近,
function unstable_scheduleCallback(priorityLevel, callback, options) {var currentTime = getCurrentTime();
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;
switch (priorityLevel) {
case ImmediatePriority:// 优先级越高 timeout 越小
timeout = IMMEDIATE_PRIORITY_TIMEOUT;//-1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;//250
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 = {// 新建 task
id: taskIdCounter++,
callback// 回调函数
priorityLevel,
startTime,// 开始工夫
expirationTime,// 过期工夫
sortIndex: -1,
};
if (enableProfiling) {newTask.isQueued = false;}
if (startTime > currentTime) {// 没有过期
newTask.sortIndex = startTime;
push(timerQueue, newTask);// 退出 timerQueue
//taskQueue 中还没有过期工作,timerQueue 中离过期工夫最近的 task 正好是 newTask
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {if (isHostTimeoutScheduled) {cancelHostTimeout();
} else {isHostTimeoutScheduled = true;}
// 定时器,到了过期工夫就退出 taskQueue 中
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);// 退出 taskQueue
if (enableProfiling) {markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);// 执行过期的工作
}
}
return newTask;
}
工作暂停之后怎么持续
在 workLoop 函数中有这样一段
const continuationCallback = callback(didUserCallbackTimeout);//callback 就是调度的 callback
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {// 判断 callback 执行之后的返回值类型
currentTask.callback = continuationCallback;// 如果是 function 类型就把又赋值给 currentTask.callback
markTaskYield(currentTask, currentTime);
} else {if (enableProfiling) {markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {pop(taskQueue);// 如果是 function 类型就从 taskQueue 中删除
}
}
advanceTimers(currentTime);
在 performConcurrentWorkOnRoot 函数的结尾有这样一个判断,如果 callbackNode 等于 originalCallbackNode 那就复原工作的执行
if (root.callbackNode === originalCallbackNode) {
// The task node scheduled for this root is the same one that's
// currently executed. Need to return a continuation.
return performConcurrentWorkOnRoot.bind(null, root);
}
Lane
Lane 的和 Scheduler 是两套优先级机制,相比来说 Lane 的优先级粒度更细,Lane 的意思是车道,相似赛车一样,在 task 获取优先级时,总是会优先抢内圈的赛道,Lane 示意的优先级有以下几个特点。
-
能够示意不同批次的优先级
从代码中中能够看到,每个优先级都是个 31 位二进制数字,1 示意该地位能够用,0 代表这个地位不能用,从第一个优先级 NoLanes 到 OffscreenLane 优先级是升高的,优先级越低 1 的个数也就越多(赛车较量外圈的车越多),也就是说含多个 1 的优先级就是同一个批次。
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;
-
优先级的计算的性能高
例如,能够通过二进制按位与来判断 a 和 b 代表的 lane 是否存在交加
export function includesSomeLane(a: Lanes | Lane, b: Lanes | Lane) {return (a & b) !== NoLanes; }
Lane 模型中 task 是怎么获取优先级的(赛车的初始赛道)
工作获取赛道的形式是从高优先级的 lanes 开始的,这个过程产生在 findUpdateLane 函数中,如果高优先级没有可用的 lane 了就降落到优先级低的 lanes 中寻找,其中 pickArbitraryLane 会调用 getHighestPriorityLane 获取一批 lanes 中优先级最高的那一位,也就是通过 lanes & -lanes
获取最左边的一位
export function findUpdateLane(
lanePriority: LanePriority,
wipLanes: Lanes,
): Lane {switch (lanePriority) {
//...
case DefaultLanePriority: {let lane = pickArbitraryLane(DefaultLanes & ~wipLanes);// 找到下一个优先级最高的 lane
if (lane === NoLane) {// 上一个 level 的 lane 都占满了降落到 TransitionLanes 持续寻找可用的赛道
lane = pickArbitraryLane(TransitionLanes & ~wipLanes);
if (lane === NoLane) {//TransitionLanes 也满了
lane = pickArbitraryLane(DefaultLanes);// 从 DefaultLanes 开始找
}
}
return lane;
}
}
}
Lane 模型中高优先级是怎么插队的(赛车抢赛道)
在 Lane 模型中如果一个低优先级的工作执行,并且还在调度的时候触发了一个高优先级的工作,则高优先级的工作打断低优先级工作,此时应该先勾销低优先级的工作,因为此时低优先级的工作可能曾经进行了一段时间,Fiber 树曾经构建了一部分,所以须要将 Fiber 树还原,这个过程产生在函数 prepareFreshStack 中,在这个函数中会初始化曾经构建的 Fiber 树
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;// 之前曾经调用过的 setState 的回调
//...
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
// 新的 setState 的回调和之前 setState 的回调优先级相等 则进入 batchedUpdate 的逻辑
if (existingCallbackPriority === newCallbackPriority) {return;}
// 两个回调优先级不统一,则被高优先级工作打断,先勾销以后低优先级的工作
cancelCallback(existingCallbackNode);
}
// 调度 render 阶段的终点
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
//...
}
function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
root.finishedWork = null;
root.finishedLanes = NoLanes;
//...
//workInProgressRoot 等变量从新赋值和初始化
workInProgressRoot = root;
workInProgress = createWorkInProgress(root.current, null);
workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
workInProgressRootExitStatus = RootIncomplete;
workInProgressRootFatalError = null;
workInProgressRootSkippedLanes = NoLanes;
workInProgressRootUpdatedLanes = NoLanes;
workInProgressRootPingedLanes = NoLanes;
//...
}
Lane 模型中怎么解决饥饿问题(最初一名赛车最初也要达到起点啊)
在调度优先级的过程中,会调用 markStarvedLanesAsExpired 遍历 pendingLanes(未执行的工作蕴含的 lane),如果没过期工夫就计算一个过期工夫,如果过期了就退出 root.expiredLanes 中,而后在下次调用 getNextLane 函数的时候会优先返回 expiredLanes
export function markStarvedLanesAsExpired(
root: FiberRoot,
currentTime: number,
): void {
const pendingLanes = root.pendingLanes;
const suspendedLanes = root.suspendedLanes;
const pingedLanes = root.pingedLanes;
const expirationTimes = root.expirationTimes;
let lanes = pendingLanes;
while (lanes > 0) {// 遍历 lanes
const index = pickArbitraryLaneIndex(lanes);
const lane = 1 << index;
const expirationTime = expirationTimes[index];
if (expirationTime === NoTimestamp) {
if ((lane & suspendedLanes) === NoLanes ||
(lane & pingedLanes) !== NoLanes
) {expirationTimes[index] = computeExpirationTime(lane, currentTime);// 计算过期工夫
}
} else if (expirationTime <= currentTime) {// 过期了
root.expiredLanes |= lane;// 在 expiredLanes 退出以后遍历到的 lane
}
lanes &= ~lane;
}
}
export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
//...
if (expiredLanes !== NoLanes) {
nextLanes = expiredLanes;
nextLanePriority = return_highestLanePriority = SyncLanePriority;// 优先返回过期的 lane
} else {//...}
return nextLanes;
}
下图更直观,随之工夫的推移,低优先级的工作被插队,最初也会变成高优先级的工作