一、react任务调度理解
Scheduler是一个独立的包,可以独自承担起任务调度的职责,你只需要将任务和任务的优先级交给它,它就可以帮你管理任务,安排任务的执行。这就是React和Scheduler配合工作的模式。
对于多个任务,它会先执行优先级高的。对于单个任务,它会有节制地去执行。不会一直占用着线程去执行任务。而是执行一会,中断一下。用这样的模式,来避免一直占用有限的资源执行耗时较长的任务,解决用户操作时页面卡顿的问题,实现更快的响应
二、使用MessageChannel进行调度
在实现调度任务时,react 通过 new MessageChannel(); 创建了消息通道,当发现 js 线程空闲时,通过 postMessage 通知 Scheduler 开始调度。然后 react 接收到调度开始的通知时,就通过 performWorkUntilDeadline 函数去执行任务,从而实现了帧空闲时间的任务调度。
// 接收 MessageChannel 消息
const performWorkUntilDeadline = () => {
// ...省略无关代码
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// 更新deadline
deadline = currentTime + yieldInterval;
// 执行callback
scheduledHostCallback(hasTimeRemaining, currentTime);
} else {
isMessageLoopRunning = false;
}
};
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
// 请求回调
requestHostCallback = function(callback) {
// 1. 保存callback
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// 2. 通过 MessageChannel 发送消息
port.postMessage(null);
}
};
// 取消回调
cancelHostCallback = function() {
scheduledHostCallback = null;
};
为啥使用MessageChannel?
如果当前环境不支持 MessageChannel 时,会默认使用 setTimeout
MessageChannel 的作用
- 生成浏览器 Eventloops 中的一个宏任务,实现将主线程还给浏览器,以便浏览器更新页面
- 浏览器更新页面后能够继续执行未完成的 Scheduler 中的任务
不用微任务迭代原因:微任务将在页面更新前全部执行完,达不到将主线程还给浏览器的目的
不适用setTimeout的原因:因为 setTimeout(fn,0) 所创建的宏任务,会有至少 4ms 的执行时差,setInterval 同理
三、两大核心功能
任务队列管理 和 时间片下任务的中断和恢复。
- 任务优先级让任务按照自身的紧急程度排序,这样可以让优先级最高的任务最先被执行到。
- 时间片规定的是单个任务在这一帧内最大的执行时间,任务一旦执行时间超过时间片,则会被打断,有节制地执行任务。这样可以保证页面不会因为任务连续执行的时间过长而产生卡顿。
3.1 任务队列管理
在Scheduler内部,把任务分成了两种:未过期的和已过期的,分别用两个队列存储,前者存到timerQueue中,后者存到taskQueue中。
3.1.1 如何区分任务是否过期?
用任务的开始时间(startTime)和当前时间(currentTime)作比较。开始时间大于当前时间,说明未过期,放到timerQueue;开始时间小于等于当前时间,说明已过期,放到taskQueue。
3.1.2 不同队列中的任务如何排序?
当任务一个个入队的时候,自然要对它们进行排序,保证紧急的任务排在前面,所以排序的依据就是任务的紧急程度。而taskQueue和timerQueue中任务紧急程度的判定标准是有区别的。
taskQueue中,依据任务的过期时间(expirationTime)排序,过期时间越早,说明越紧急,过期时间小的排在前面。过期时间根据任务优先级计算得出,优先级越高,过期时间越早。
timerQueue中,依据任务的开始时间(startTime)排序,开始时间越早,说明会越早开始,开始时间小的排在前面。任务进来的时候,开始时间默认是当前时间,如果进入调度的时候传了延迟时间,开始时间则是当前时间与延迟时间的和。
3.1.3 任务入队如何执行?
如果放到了taskQueue,那么立即调度一个函数去循环taskQueue,挨个执行里面的任务。
如果放到了timerQueue,那么说明它里面的任务都不会立即执行,那就等到了timerQueue里面排在第一个任务的开始时间,看这个任务是否过期,如果是,则把任务从timerQueue中拿出来放入taskQueue,调度一个函数去循环它,执行掉里面的任务;否则过一会继续检查这第一个任务是否过期。
任务队列管理相对于单个任务的执行,是宏观层面的概念,它利用任务的优先级去管理任务队列中的任务顺序,始终让最紧急的任务被优先处理
3.2 时间切片的概念理解:
-
在理解切片的过程中,我一直思考错了方向,总想着是先把任务按时间切好之后,再顺次执行,以此达到切片的效果
-
其实是另一种实现,举个例子:比如我们切萝卜,并不是先标记好每一段在哪才下手,而是达到一定长度就下手,最终实现了按将萝卜切成相似的一段一段
-
有了这层思考之后,理解切片,其实就是到时间点就停止,到时间点就停止,以此循环,最终看到的结果便是按一定时间段切割的效果
3.3 任务的中断与恢复:
主要的实现代码在workloop中,workLoop可以分为两大部分:循环taskQueue执行任务 和 任务状态的判断。
任务中断的理解:
currentTask是当前正在执行的任务,中止的判断条件是:任务并未过期,但已经没有剩余时间了,或者应该让出执行权给主线程(时间片的限制)。但是被break的只是while循环,while下部还是会判断currentTask的状态。
由于它只是被中止了,所以currentTask不可能是null,那么会return true告诉外部还没执行完(此处是恢复任务的关键)。如果taskQueue已经被清空了,return false 让外部终止本次调度。而workLoop的执行结果会被flushWork return出去,flushWork实际上是scheduledHostCallback,当performWorkUntilDeadline检测到scheduledHostCallback的返回值(hasMoreWork)为false时,就会停止调度。
// 任务并未过期,但已经没有剩余时间了,或者应该让出执行权给主线程(时间片的限制)
if (currentTask.expirationTime > currentTime && (!hasTimeRemaining || shouldYieldToHost())) {
// break掉while循环
break
}
任务恢复的理解:
任务被中断之后,在 scheduler 中的 workLoop 发现 continuationCallback 返回的值为一个方法,会存下当前中断的回调,且不让当前执行的任务出栈,也就意味着当前的 task 没有执行完。浏览器有空闲的时间之后,就会执行先执行这个continuationCallback函数,也就实现了任务恢复