阅读图解React笔记5
2022 react本文仅仅是阅读 图解 React 原理系列 的笔记,了解更多内容请查看原文链接。
调度原理
调度实现
调度中心最核心的代码,在 scheduler/src/forks/SchedulerHostConfig.default.js 中。该文件共导出 8 个函数。
/** 请求及时回调 */
export let requestHostCallback;
/** 取消及时回调 */
export let cancelHostCallback;
/** 请求延时回调 */
export let requestHostTimeout;
/** 取消延时回调 */
export let cancelHostTimeout;
/** 是否让出主线程 */
export let shouldYieldToHost;
/** 请求绘制 */
export let requestPaint;
/** 获取当前时间 */
export let getCurrentTime;
/** 强制设置 yieldInterval */
export let forceFrameRate;
调度相关
与调度相关的,请求或取消调度,它们的目的是请求执行(或取消)回调函数
- requestHostCallback
- cancelHostCallback
- requestHostTimeout
- cancelHostTimeout
const performWorkUnitlDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 在 yieldInterval 毫秒后产生,无论我们在 vsync 周期中的哪个位置
// 这意味着在消息时间开始时总是有剩余时间
deadline = currentTime + yieldInterval;
const hasTimeRemaining = true;
try {
// 执行回调,返回是否还有剩余任务
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
if (!hasMoreWork) {
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 如果还有更多的工作,在前一个消息事件的末尾安排下一个消息事件
port.postMessage(null);
}
} catch (error) {
// 如果调度任务抛错,退出当前浏览器任务以便观察错误
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUnitlDeadline;
requestHostCallback = function (callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
cancelHostCallback = function () {
scheduledHostCallback = null;
};
requestHostTimeout = function (callback, ms) {
taskTimeoutId = setTimeout(() => {
callback(getCurrentTime());
}, ms);
};
cancelHostTimeout = function () {
clearTimeout(taskTimeoutId);
taskTimeoutId = -1;
};
请求回调之后 scheduledHostCallback = callback,然后通过 MessageChannel 发消息的方式触发 performWorkUnitlDeadline,最后执行回调 scheduledHostCallback。
需要注意:MessageChannel 在浏览器事件循环中属于宏任务,所以调度中心用于是异步执行回调函数。
时间切片
时间切片(time slicing),执行时间分割,让出主线程(把控制权归返浏览器,浏览器可以处理用户输入,UI 绘制等紧急任务)
- getCurrentTime
- shouldYieldToHost
- requestPaint
- forceFrameRate
if (
enableIsInputPending &&
navigator !== undefined &&
navigator.scheduler !== undefined &&
navigator.scheduler.isInputPending !== undefined
) {
const scheduling = navigator.scheduling;
shouldYieldToHost = function () {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
if (needsPaint || scheduling.isInputPending()) {
return true;
}
return currentTime >= maxYieldInterval;
} else {
return false;
}
};
requestPaint = function () {
needsPaint = true;
};
} else {
shouldYieldToHost = function () {
return getCurrentTime >= deadline;
};
requestPaint = function () {};
}
forceFrameRate = function (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;
}
};
shouldYieldToHost 的判定条件:
- currentTime >= deadline(currentTime + yieldInterval),只有时间超过 deadline 之后才会让出主线程。
- yieldInterval 默认是 5ms,只能通过 forceFrameRate 函数进行修改。
- 如果一个 task 运行时间超过 5ms,下一个 task 执行之前,会把控制权归还浏览器。
- navigator.scheduling.isInputPending,用于判断是否有输入事件。
从上面可以得到调度中心的内核实现图:
任务队列管理
调度的目的是为了消费任务,接下来看看任务队列的管理和实现。
在 scheduler/src/Scheduler.js 维护一个 taskQueue,任务队列管理主要就是围绕 taskQueue 展开。
// 任务队列保存在一个最小堆里
var taskQueue = [];
var timerQueue = [];
创建任务
在 scheduler/src/Scheduler.js 中的 unstable_schedulerCallback 函数:
function unstable_schedulerCallback(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 = IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT;
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT;
break;
case LowPirority:
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 (startTime > currentTime) {
// 这是个延迟任务
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 所有的任务都是延迟的,且该任务是最先被延迟的
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
// 省略无关代码
// 请求调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
消费任务
创建任务之后,最后请求调度 requestHostCallback(flushWork),flushWork 函数作为参数传入调度中心内核等待回调。
function flushWork(hasTimeRemaining, initialTime) {
// 省略无关代码
// 做好全局标记,表示现在已经进入调度阶段
isHostCallbackScheduled = false;
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
// 循环消费队列
return workLoop(hasTimeRemaining, initialTime);
} finally {
// 还原全局标记
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
flushWork 中调用了 workLoop,队列消费的主要逻辑是在 workLoop 函数中,这就是 React 的任务调度循环。
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (currentTask.expirationTime > currentTask && (!hasTimeRemaining || shouldYieldToHost())) {
// 当前任务未过期,但是达到时间限制
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// 返回,不管是否有额外的工作
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
workLoop 就是一个大循环,在此处实现了时间切片和 fiber 树的可中断渲染。这 2 个大特性的实现,都集中于这个 while 循环。
每一次 while 循环的推出就是一个时间切片,深入分析 while 循环的退出条件:
- 队列被完全清空:正常的退出。
-
执行超时:在消费 taskQueue 时,在执行 task.callback 之前,都会检测是否超时,所以超时检测是以 task 为单位。
如果某个 task.callback 执行时间太久也会超时(如 fiber 树过大,逻辑很重),这时使用检测机制,超时了立刻暂停 task.callback 的执行
时间切片原理
消费任务队列的过程中,可以消费 1~n 个 task,甚至清空整个 queue。但是在每一次具体执行 task.callback 之前都要进行检测,如果超时可以立即推出循环并等待下一次调用。
可中断渲染
在时间切片的基础之上,如果单个 task.callback 执行时间就很长(假设 200ms)。就需要 task.callback 自己能够检测是否超时,所以在 fiber 树构建过程中,每构建完成一个单元,都会检测一次超时,如遇超时就会退出 fiber 树循环构建,并返回一个新的回调函数(就是 continuationCallback)并等待下一次回调继续未完成的 fiber 树构造。
节流防抖
在 reconciler 运作流程中的总结的 4 个阶段中,注册调度任务属于第 2 个阶段,核心逻辑位于 react-reconciler/src/ReactFiberWorkLoop.new.js ensureRootIsScheduled 函数中。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
const existingCallbackNode = root.callbackNode;
// 检测是否有车道被其他工作占用,如果有,将它们标记为已过期
markStarvedLanesAsExpired(root, currentTime);
// 确定下一个要处理的的车道及其优先级
const nextLanes = getNextLanes(
root,
root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes
);
if (nextLanes === NoLanes) {
if (existingCallbackNode !== null) {
cancelCallback(existingCallbackNode);
root.callbackNode = null;
root.callbackPriority = NoLanePriority;
}
return;
}
// 节流防抖
if (existingCallbackNode !== null) {
const existingCallbackPriority = root.callbackPriority;
if (existingCallbackPriority === newCallbackPriority) {
return;
}
cancelCallback(existingCallbackNode);
}
// 调度一个新的任务
let newCallbackNode;
if (newCallbackPriority === SyncLanePriority) {
newCallbackNode = scheduledHostCallback(performSyncWorkOnRoot.bind(null, root));
} else if (newCallbackPriority === SyncBatchedLanePriority) {
newCallbackNode = scheduleCallback(
ImmediateSchedulerPriority,
performSyncWorkOnRoot.bind(null, root)
);
} else {
const schedulerPriorityLevel = lanePriorityToSchedulerPriority(newCallbackPriority);
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root)
);
}
root.callbackPriority = newCallbackPriority;
root.callbackNode = newCallbackNode;
}
正常的情况下,ensureRootIsScheduled 函数会与 scheduler 包通信,最后注册一个 task 并等待回调。
- 在 task 注册完成之后,会设置 FiberRoot 对象上的属性,代表现在已经处于调度进行中。
-
再次进入 ensureRootIsScheduled 时,如果发现处于调度中,则需要一些节流和防抖措施,进而保证调度性能。
- 节流(判断条件:existingCallbackPriority === newCallbackPriority,新旧更新的优先级相同),则无需注册新 task,直接退出调用。
- 防抖(判断条件:existingCallbackPriority !== newCallbackPriority,新旧更新的优先级不同),则取消旧 task,重新注册新 task。