前言
React 16 后引入了 Scheduler(调度器)的概念,它是一个优先级队列,会根据任务的优先级,调度任务队列,取出优先级最高的任务去执行。当任务的时间超 5ms 后,Scheduler 会中断没完成的任务,交出 JS 线程的控制器给浏览器,避免界面交互卡顿。然后在下一次同样根据优先级,执行新任务或恢复上次未执行完的任务。
那么 Scheduler 具体是如何工作的呢?
任务队列与调度者创建
Scheduler 里面有两个优先级任务队列:
- taskQueue:
存放正常的任务
- timerQueue:
存放延时任务
taskQueue、timerQueue 都是最小堆的数据结构
当 Scheduler 开始调度时,会先从 taskQueue 中,根据优先级
,拿出对应的任务执行。执行完毕后,会将这个任务从 taskQueue 中删除。当 taskQueue 中的任务全部完成后,会去 timerQueue 中看有没有过期的任务,有的话就放到 taskQueue 中 执行。
相关代码如下:
function unstable_scheduleCallback(priorityLevel, callback, options) {
/**
第一部分
**/
var currentTime = getCurrentTime(); //当前时间
var startTime; // 开始时间
// 根据是否存在 delay,计算开始时间
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 LowPriority:
timeout = LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT;
break;
}
// 任务过期时间:开始时间 + 延时时间
var expirationTime = startTime + timeout;
/**
第二部分
**/
// 创建新任务
var newTask = {
id: taskIdCounter++,
callback, // callback = performConcurrentWorkOnRoot
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
//开始时间大于当前时间,表示当前任务是一个延时任务
if (startTime > currentTime) {
// 以开始时间作为优先级排序
newTask.sortIndex = startTime;
// 将这个延时任务加入到 timerQueue 中
push(timerQueue, newTask);
//当 taskQueue 中执行完所有的任务,且 timerQueue 有延时任务
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 创建一个timeout 作为调度者,回调函数是 handleTimeout
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 否则,是个正常任务,将 expireationTime 作为优先级排序字段
newTask.sortIndex = expirationTime;
//加入 taskQueue 中
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
//判断 Scheduled 是否正在调度任务
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
//没有的话则创建一个调度者,有的话则直接使用上一个调度者,然后开始调度任务
requestHostCallback(flushWork);
}
}
return newTask;
}
上面的代码的流程是:
- 根据是否存在 delay,
计算 startTime 开始时间
,根据任务优先级,计算 expirationTime 过期时间
- 根据条件
startTime > currentTime
,判断是正常任务(false)还是延时任务(true)若是延时任务
,则以 startTime 作为优先级排序字段,加入 timerQueue
,然后判断 taskQueue 中任务是否都已经完成,如果都已经完成,则调用requestHostTimeout(handleTimeout, startTime - currentTime)
创建一个 timeout 作为调度者。若是正常任务
,则以 expirationTime 作为优先级排序字段,加入 taskQueue
,判断当前有没有调度者,如果没有,则调用requestHostCallback(flushWork)
创建调度者,否则使用上一次的调度者,然后调度任务。
清楚了上面的过程后,我们继续看一下后续流程是什么。
当任务是延时任务时,会进入 handleTimeout
回调,而这个函数的代码如下:
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
//检查 timerQueue 中是否有已过期的任务
//有的话则将过期任务添加到 taskQueue 进行执行
advanceTimers(currentTime);
//isHostCallbackScheduled:是否有正在执行的调度,如果没有,则会创建一个调度者,去执行任务
if (!isHostCallbackScheduled) {
// 如果 taskQueue 里面还有任务没执行完,走 requestHostCallback,创建调度者
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
// 否则,把 timerQueue 中的任务拿出来,如果存在延时任务
// 则通过 requestHostTimeout 创建 timeout 作为调度者
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
handleTimeout 做的事情是:
- 通过 advanceTimers 函数,判断 timerQueue 里面有没有过期的任务,如果有,放入 taskQueue
- 判断当前是不是有调度者正在调度任务,
如果没有,会去创建调度者
,分两种情况- 如果此时 taskQueue 还有任务没执行完,则调用
requstHostCallback
创建调度者 - 否则,如果 timerQueue 中有延时任务没执行,则调用
requestHostTimeout
创建一个 timeout 作为调度者
- 如果此时 taskQueue 还有任务没执行完,则调用
那么 requestHostCallback 里面有干了什么?
// 注意:传入的 callback 就是前文代码里面的 flushWork
function requestHostCallback(callback) {
// 记录 flushWork
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
//调用这个方法
schedulePerformWorkUntilDeadline();
}
}
// 与 schedulePerformWorkUntilDeadline 相关的代码
let schedulePerformWorkUntilDeadline;
// 分三种情况创建调度者
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
} else {
schedulePerformWorkUntilDeadline = () => {
localSetTimeout(performWorkUntilDeadline, 0);
};
}
上面的代码流程是:
- scheduledHostCallback 变量 记录传入的 callbak,也就是 flushWork
根据不同的执行环境
,分三种情况创建调度者:- 如果是老版本 IE,则使用 setImmediate 作为调度者
- 如果是默认浏览器环境,则使用 MessageChannel 作为调度者
- 如果前两种情况都不能实现,则使用 setTimeout 作为调度者
那么我们现在在这里总结下前面的流程:
确认任务类型:
根据 currentTime 当前时间、是否存在 delay、任务优先级,计算 startTime、expirationTime 用于优先级排序的字段,然后判断是 newTask 正常任务还是延时任务,然后加入到 taskQueue 或 timerQueue 中。创建调度者:
延时任务创建 timeout 作为创建者,正常任务根据当前执行环境,可以创建 setImmediate、MessageCahnnel、setTimeout 三种类型的调度者。
到这里,任务队列分别存储了对应类型的任务,并以相关规则优先级排序,调度者也已经被创建。那么接下来看如何调度任务的。
执行任务调度
上面创建调度者的代码中,三种类型最终都会调用 performWorkUntilDeadline 函数,该函数就是执行任务的函数。
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
startTime = currentTime;
const hasTimeRemaining = true;
let hasMoreWork = true;
try {
// 调用 scheduledHostCallback 函数
// 这个函数其实就是 requestHostCallback(flushWork) 中的 flushWork
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
// hashMore 表示是否还有任务需要执行
// 如果为 true 表示有任务在执行中被中断,需要重新执行,那么则需要重新发起一个调度
if (hasMoreWork) {
// 重新发起一个调度,也就是那三种情况下创建调度者
schedulePerformWorkUntilDeadline();
} else {
// 否则就表示 taskQueue 中的任务都执行完成了
// 需要将调度者释放,为下一次调度做准备
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
needsPaint = false;
};
在 performWorkUntilDeadline 中,会先调用 scheduledHostCallback 函数,而这个函数其实就是之前 requestHostCallback(flushWork) 中的 flushWork
function flushWork(hasTimeRemaining, initialTime) {
...
return workLoop(hasTimeRemaining, initialTime);
...
}
flushWork 内部调用 workLoop 函数,将 workLoop 的返回值返回出去,赋值给了 hasMoreWork 变量
也就是说,真正执行任务的地方是在 workLoop 中
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
//检查 timerQueue 中是否有过期的任务,有就添加到 taskQueue 中执行
advanceTimers(currentTime);
//取出优先级最高的任务
currentTask = peek(taskQueue);
// work循环
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
//当前任务的过期时间是否大于当前时间,大于则表示没有过期则不需要立即执行
//hasTimeRemaining: 表示是否还有剩余时间,剩余时间不足则需要中断当前任务,让其他任务先执行
//shouldYieldToHost: 是否应该中断当前任务
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
break;
}
//判断当前任务的回调函数 callback 是否为空,为空则会将当前任务从任务队列中删除
const callback = currentTask.callback;
if (typeof callback === 'function') {
//回调函数设置为null,表示任务执行完成后会从 taskQueue 中删除
currentTask.callback = null;
//获取任务的优先级
currentPriorityLevel = currentTask.priorityLevel;
//判断当前任务是否过期
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
//执行任务,记录执行完成后的结果
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
//任务执行完成后的结果返回的是一个函数表示当前任务没有完成
//则将这个函数作为当前任务新的回调函数,在下一次While循环时调用
currentTask.callback = continuationCallback;
} else {
//否则,删除当前任务
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
//检查 timerQueue 中是否有过期的任务,有就加入 taskQueue 中
advanceTimers(currentTime);
} else {
//删除当前任务
pop(taskQueue);
}
//从taskQueue中继续获取任务,如果上一次任务没有完成,那么不会从taskQueue中删除,获取的还是上一次的任务
//接下来会继续执行它
currentTask = peek(taskQueue);
}
//当前任务被中断,currentTask则不会为null,则会返回true,
//scheduler会继续发起调度,执行任务
if (currentTask !== null) {
return true;
} else {
//currentTask为 null,则表示 taskQueue 中的任务都执行完成了
//则判断 timerQueue 中是否有过期任务
//有的话则添加到 taskQueue 中,并重新发起调度执行任务
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
workLoop 中的流程是:
-
判断是否终止当前任务,判断的条件是:
currentTask.expirationTime > currentTime
:当前任务的过期时间是否大于当前时间,如果大于,则表示还没过期,让权给其他已经过期的任务!hasTimeRemaining
:是否还有剩余时间,如果没有,则需要中断当前任务shouldYieldToHost()
:这个函数其实会先判断当前任务的执行时间是否小于 5ms,如果是,返回 false 不需要中断,如果超过了,则返回 true 中断
-
如果不中断当前任务,则执行当前任务的执行函数:
- currentTask.callback 是否是 function
- 如果不是,从 taskQueue 中删除该任务
- 否则执行 callback,将执行的结果记录给
continuationCallback
变量,continuationCallback 如果是 null,表示当前任务执行完毕,将它从 taskQueue 中删除。否则没执行完,就让权给其他优先级更高的任务。
- currentTask.callback 是否是 function
最后,拿到结果,回到 performWorkUntilDeadline 函数中:
else {
// 否则就表示 taskQueue 中的任务都执行完成了
// 需要将调度者释放,为下一次调度做准备
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
走到这里的时候 Scheduler 的调度就结束了。
总结
任务队列管理:
每个React任务都有开始时间(StartTime
)和任务到期时间(expirationTime
),Scheduler 使用两个优先队列存储任务 TaskQueue
、TimerQueue
存放任务,前者存放即将执行的任务,后者则存放延时执行任务:
- TaskQueue 是以
expirationTime 作为优先级排序字段
,到期时间小的排在前面。taskQueue 中的任务会不断执行 - TimerQueue 中任务是以
startTime = currentTime + delay 作为优先级排序字段
,在 timerQueue 中的任务会采用 setTimeout 定时器,等到任务等待时间过后再放到 taskQueue 中 **
创建调度者:
React 会根据当前执行环境,创建不同的调度者,即:setImmediate、MessageChannel、setTimeout 三种类型
- 老版本IE下,使用 setImmediate 作为调度者
- 浏览器默认使用 MessageChannel 作为调度者
- 当上面都无法满足时,使用 setTimeout 作为调度者
执行任务调度时:
会首先判断当前任务 currentTask 是否会被中断
-
如果任务被中断,
会去 taskQueue 中拿任务,如果 taskQueue 中没有任务,去 timerQueue 中拿任务
,然后继续执行任务调度 -
如果任务没有被中断,会判断当前执行的任务的执行回调函数 currentTask.callback 是否为空
-
如果为空则会将 currentTask 从 taskQueue 中删除
-
如果不为空,则执行 currentTask.callback,将执行的结果记录到 continuationCallback 变量中,判断 continuationCallback 是否为空
- 如果不为空,表示 currentTask 还没执行完,将 continuationCallback 作为 currentTask 下一次任务调度时的 callback,即:
currentTask.callback = continuationCallback
- 如果为空,表示 currentTask 执行完毕,然后从 taskQueue 中删除
- 如果不为空,表示 currentTask 还没执行完,将 continuationCallback 作为 currentTask 下一次任务调度时的 callback,即:
-
-
重复上面的步骤,不断消费 taskQueue 中的任务。当 taskQueue 为空,表示 React 所有任务执行完毕,最后会
将调度者释放 scheduledHostCallback = null
,为下一次调度做准备。
结语
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论