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。