React的任务调度器-Scheduler
在 React 18 中,React 将铺垫了两个版本的并发渲染模式正式转正,默认开启该模式。React 18 引入了全新的任务调度器,作为并发渲染模式的一部分。该调度器不仅是 React 的一部分,还是一个独立的开源库,为外部提供使用。本文将深入讨论 React 18 的任务调度器,包括任务单元、调度方式以及调度过程。
# 任务单元
任务调度器中,任务是最小的调度单元,通过优先队列的数据结构排列。每个任务包含了唯一的 ID、回调函数、优先级以及过期时间等信息。React 使用最小堆(优先队列)的方式进行任务排序,确保按照优先级和过期时间执行。
var newTask: Task = {
id: taskIdCounter++, // 唯一id
callback, // 任务的回调执行
priorityLevel, // 任务的优先级
startTime, // 任务的开始时间
expirationTime, // 任务的过期时间
sortIndex: -1, // 堆的排序index, 跟过期时间的值一致
};
任务调度器共有五个优先级,每个优先级对应不同的过期时间,调度器按照过期时间对任务进行排序,优先执行最早过期的任务。
// Times out immediately
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
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;
}
# 调度方式
由于 JavaScript 引擎是单线程执行的,执行过程中是不能中断并且会阻塞渲染或者其他计算。调度器为了实现中断的效果,通过宏任务的方式去执行分片。根据环境不同,可能使用**setImmediate(nodejs或ie)
、MessageChannel(优先)
或setTimeout(兜底)
**等方式。因为**setTimeout**
就算是延迟 0 秒,浏览器还是默认给 4ms 的间隔去执行。
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
// There's a few reasons for why we prefer setImmediate.
//
// Unlike MessageChannel, it doesn't prevent a Node.js process from exiting.
// (Even though this is a DOM fork of the Scheduler, you could get here
// with a mix of Node.js 15+, which has a MessageChannel, and jsdom.)
// https://github.com/facebook/react/issues/20756
//
// But also, it runs earlier which is the semantic we want.
// If other browsers ever implement it, it's better to use it.
// Although both of these would be inferior to native scheduling.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
// We should only fallback here in non-browser environments.
schedulePerformWorkUntilDeadline = () => {
// $FlowFixMe[not-a-function] nullable value
localSetTimeout(performWorkUntilDeadline, 0);
};
}
# 调度过程
在调度器中,拥有两个队列(堆),一个是任务队列,一个延迟队列。当调用调度方法(unstable_scheduleCallback)的时候传入的第三个参数指定 delay 的时候,该任务才会进入延迟队列,否则进入任务队列。
进行调度循环的时候,因为队列顶部任务的过期时间都是最快的,所以会获取顶部的任务判断该任务是否过期,把所有过期的任务都执行。剩下没过期的任务,先判断 startTime 和 currTime 比较是否大于 5ms(在之前的版本中,会根据浏览器的 fps 去进行判断,最新版本直接写死 5ms 了),如果是,就执行,否则进入延迟队列的判断,把已经超过延迟时间的任务都取出来放入任务队列中,继续循环。如果都没有任务,这次调度结束,等待下一次的调度。
function workLoop(initialTime: number) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
// $FlowFixMe[incompatible-use] found when upgrading Flow
const callback = currentTask.callback;
if (typeof callback === 'function') {
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = null;
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentPriorityLevel = currentTask.priorityLevel;
// $FlowFixMe[incompatible-use] found when upgrading Flow
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskRun(currentTask, currentTime);
}
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
// If a continuation is returned, immediately yield to the main thread
// regardless of how much time is left in the current time slice.
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.callback = continuationCallback;
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskYield(currentTask, currentTime);
}
advanceTimers(currentTime);
return true;
} else {
if (enableProfiling) {
// $FlowFixMe[incompatible-call] found when upgrading Flow
markTaskCompleted(currentTask, currentTime);
// $FlowFixMe[incompatible-use] found when upgrading Flow
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
advanceTimers(currentTime);
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
# 结语
任务调度器为 React 带来了更高效的任务处理机制。通过任务单元的最小化、多优先级的排序、中断式调度等特性,React 在不同环境中都能提供流畅的用户体验。