react可以将大任务分解为多个小任务,分配到多个浏览器每帧下的空余时间执行,这就是我们常说的时间切片。这样就不会阻塞浏览器的绘制任务,造成页面卡顿。在这个过程中,react是如何实现任务的调度,并且如何实现时间切片的呢。
1.时间切片
浏览器如何控制react更新的呢。我们知道浏览器在绘制一帧的时候会处理很多事物,包括事件处理,js执行布局,绘制页面等等。
同时谷歌浏览器提供了requestIdleCallback API。这个api可以在“浏览器重排/重绘”后如果当前帧还有空余时间时被调用的。听起来这是个完美实现时间切片的api,但由于兼容性的问题。react并没有使用requestIdleCallback,而是模拟实现了requestIdleCallback,这就是Scheduler。
2.模拟requestIdleCallback
为了能模拟出requestIdleCallback,必须要做到以下两点。
- 可以主动让出线程,让浏览器执行其他任务。
- 在每帧下只执行一次,然后在下一帧中继续请求时间片。
能满足以上两种情况的便只有宏任务,而在宏任务中首选便是setTimeout。但是由于setTimeout会有4ms的时差,react放弃使用了setTimeout,改用了MessageChannel。(在不兼容Messagechannel的情况下依然使用setTimeout实现)
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function schedulePerformWorkUntilDeadline() {
port.postMessage(null);
}
3.任务调度
- 首先我们入口函数,就是我们外部调度需要调用这个函数,这里的关键点就是我们根据优先级,生成任务的过期时间和最小堆的排序依据,因为优先级更高,过期时间肯定越短。然后生成Task,塞到最小堆taskQueue中。然后开始去调用
requestHostCallback函数调度任务。
function scheduleCallback(priorityLevel: PriorityLevel, callback: Callback) {
const startTime = getCurrentTime();
let timeout: number;
switch (priorityLevel) {
case ImmediatePriority:
timeout = -1;
break;
case UserBlockingPriority:
timeout = userBlockingPriorityTimeout;
break;
case NormalPriority:
timeout = normalPriorityTimeout;
break;
case LowPriority:
timeout = lowPriorityTimeout;
break;
case IdlePriority:
timeout = maxSigned31BitInt;
break;
default:
timeout = normalPriorityTimeout;
}
const expirationTime = startTime + timeout;
const newTask: Task = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
//最小堆排序依据, 最小的在堆顶,过期时间越小,说明优先级越高
sortIndex: expirationTime,
};
push(taskQueue, newTask);
//只有一个主线程,看主线程是否在调度任务,时间切片是否在执行任务
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
}
- 调用
requestHostCallback本质上就是发起了一次MessageChannel的调用,产生了一个宏任务。最终会执行workloop。
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
function schedulePerformWorkUntilDeadline() {
port.postMessage(null);
}
function performWorkUntilDeadline() {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
//记录一个Work的起始时间,其实就是一个时间切片的起始时间
startTime = currentTime;
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
}
- 在 workLoop 中会先检测一遍是否有任务过期,然后取出最先过期的任务执行。执行结果如果还是函数,就return出去,放在下一个宏任务执行, 如果不是函数则从任务队列清除这个任务, 最后再从任务队列取出新的任务, 开始while循环, 判断到如果任务到期了或者控制权应该给主线程,就跳出循环。
function workLoop(initialTime: number): boolean {
let currentTime = initialTime;
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (currentTask.expirationTime > currentTime && 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);
if (typeof continuationCallback === "function") {
currentTask.callback = continuationCallback;
return true;
} else {
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
return false;
}
自己的理解