本篇已收录到掘金专栏《React 基础与进阶》,该系列目前一共 16 篇。
欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。
前言
本篇我们接着《React 之 Scheduler 源码解读(上)》,讲解延时任务的执行源码。
scheduleCallback
依然从 unstable_scheduleCallback
这个入口函数说起:
var isHostTimeoutScheduled = false;
function unstable_scheduleCallback(priorityLevel, callback, options) {
// ...
// 如果是延时任务,将其放到 timerQueue
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);
}
}
// 如果是普通任务,就将其放到 taskQueue
else {
// ...
}
return newTask;
}
普通任务在创建后,会放入 taskQueue 中,直接安排调度,但具体任务时候执行,则要看调度器 Scheduler 的安排。
而所谓延时任务,指的是延时安排调度的任务,它会有一个指定的 delay 值,表示具体延时多久,通过 delay + currentTime,我们可以算出安排调度的具体时间,也就是 startTime。
对于延时任务,我们会将其放入 timerQueue 队列。
然后我们进行了判断:
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
}
如果 taskQueue 没有任务,并且创建的这个任务就是最早的延时任务,那就执行 cancelHostTimeout
,这样做保证了只有一个 requestHostTimeout
在执行,那 requestHostTimeout
和 cancelHostTimeout
做了什么呢?
requestHostTimeout
let taskTimeoutID = -1;
function requestHostTimeout(callback, ms) {
taskTimeoutID = setTimeout(() => {
callback(getCurrentTime());
}, ms);
}
requestHostTimeout
就是一个 setTimeout 的封装,所谓延时任务,就是一个延时安排调度的任务,怎么保证在延时时间达到后立刻安排调度呢,React 就用了 setTimeout,计算 startTime - currentTime 来实现,我们也可以想出,handleTimeout 的作用就是安排调度。
那 cancelHostTimeout
代码我们也很容易想到了:
cancelHostTimeout
function cancelHostTimeout() {
clearTimeout(taskTimeoutID);
taskTimeoutID = -1;
}
结合 unstable_scheduleCallback
、requestHostTimeout
、cancelHostTimeout
的代码,我们可以了解到:
在 Scheduler 中,最多只有一个定时器在执行(requestHostTimeout),时间为所有延时任务中延时时间最小的那个,如果创建的新任务是最小的那个,那就取消掉之前的,使用新任务的延时时间再创建一个定时器,定时器到期后,我们会将该任务安排调度(handleTimeout)
但这个逻辑只在 taskQueue 没有任务的时候,如果 taskQueue 有任务呢?
如果 taskQueue 有任务,在每个任务完成的时候,React 都会调用 advanceTimers ,检查 timerQueue 中到期的延时任务,将其转移到 taskQueue 中,所以没有必要再检查一遍了。
总结一下:如果 taskQueue 为空,我们的延时任务会创建最多一个定时器,在定时器到期后,将该任务安排调度(将任务添加到 taskQueue 中)。如果 taskQueue 列表不为空,我们在每个普通任务执行完后都会检查是否有任务到期了,然后将到期的任务添加到 taskQueue 中。
但这个逻辑里有一个漏洞:
我们新添加一个普通任务,假设该任务执行时间为 5ms,再添加一个延时任务,delay 为 10ms。
因为创建延时任务的时候 taskQueue 中有值,所以不会创建定时器,当普通任务执行完毕后,我们执行 advanceTimers,因为延时任务没有到期,所以也不会添加到 taskQueue 中,那么这个延时任务就不会有定时器让它准时进入调度。如果没有新的任务出现,它永远都不会执行。
所以在 workLoop 函数的源码中,有这样一段代码:
function workLoop(hasTimeRemaining, initialTime) {
advanceTimers(currentTime);
// ...
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
我们执行完任务,如果 taskQueue 为空,并且 timerQueue 中还有任务,那我们就再创建一个定时器。
handleTimeout
接下来我们看看 handleTimeout 的源码,这个函数执行的时候,该延时任务已经到期:
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
可以看到首先调用了 advanceTimers,将到期的延时任务转移到 taskQueue 中。
如果 taskQueue 不为空,那就执行 requestHostCallback,告诉浏览器,等空了就干活,继续遍历执行 taskQueue 中的任务。
而如果 taskQueue 为空,嗯?为什么会为空呢?
既然 handleTimeout 执行了,说明这个延时任务一定是到期了,我们执行 advanceTimers,taskQueue 中一定有任务,这里肯定不为空呀。
这里我们要考虑一种情况,那就是延时任务可能被取消了,但这个取消不是 cancelHostTimeout,执行 cancelHostTimeout,我们只是移除了定时器,延时任务还是保存在 timerQueue 中,我们说的取消,是真正的取消,取消的方式是将任务对象 task 的 callback 函数置为 null。当 React 执行 advanceTimers 的时候,advanceTimers 会判断 callback 函数的值,如果为空,表示完成或者清除,那就从任务列表中移除掉。
所以如果我们发起一个延时任务,然后将该延时任务取消,当执行 handleTimeout 的时候,peek(taskQueue)
的结果就会为空,此时怎么解决呢?
很简单,那就根据现有的 timerQueue 中的任务,新开启一个定时器好了。
总结:延时任务流程
当我们创建一个延时任务后,我们将其添加到 timerQueue 中,我们使用 requestHostTimeout 来安排调度,requestHostTimeout 本质是一个 setTimeout,当时间到期后,执行 handleTimeout,将到期的任务转移到 taskQueue,然后按照普通任务的执行流程走。
flushWork 中的 cancelHostTimeout
function flushWork(hasTimeRemaining: boolean, initialTime: number) {
if (isHostTimeoutScheduled) {
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
// ...
return workLoop(hasTimeRemaining, initialTime);
}
我们在执行 flushWork 的时候,如果有正在执行的定时器,我们会执行 cancelHostTimeout 取消定时器,这里为什么要取消呢?
定时器的目的表面上是为了保证最早的延时任务准时安排调度,实际上是为了保证 timerQueue 中的任务都能被执行。定时器到期后,我们会执行 advanceTimers 和 flushWork,flushWork 中会执行 workLoop,workLoop 中会将 taskQueue 中的任务不断执行,当 taskQueue 执行完毕后,workLoop 会选择 timerQueue 中的最早的任务重新设置一个定时器。所以如果 flushWork 执行了,定时器也就没有必要了,所以可以取消了。
至此,React 的 Scheduler 的源码解读第一遍就结束了,接下来我们会补充讲解 Scheduler 的细节实现、提供可供直接使用的源码版本,原理总结,帮助大家更好的认识 Scheduler。
React 系列
该系列带大家从源码的角度深入理解 React 的各个 API 和执行过程。
本篇已收录到掘金专栏《React 基础与进阶》。该系列目前一共 16 篇。
此外我还写过 JavaScript 系列、TypeScript 系列、React 系列、Next.js 系列、冴羽答读者问等 14 个系列文章, 全系列文章目录:github.com/mqyqingfeng…
通过文字建立交流本身就是一种缘分,欢迎围观我的“朋友圈”、加入“低调务实优秀中国好青年”前端社群,分享技术,带你成长。