面试官问我 react scheduler 调度机制原理? 我却支支吾吾答不上来...😭😭
在一次面试时,面试官问我 react scheduler 调度机制原理是什么?我却支支吾吾想了半天没答上来,看着面试官轻蔑的表情,我在心里默默的下定决心:三十年河东三十年河西,莫欺少年穷......
什么是 scheduler?
scheduler
(协调器)是react的更新流程中非常重要的一环。只需要将任务和任务的优先级交给它,它就可以帮你管理任务,安排任务的执行。
在涉及到例如大量的dom更新操作,如果一直同步执行这个耗时非常久的任务,就会一直占用着线程,所以就会造成用户在使用浏览器时视觉上的卡顿。
scheduler
对于更新的方式上做出优化:对于单个任务来说,会有节制地去执行,不会一直占用着线程去执行任务。而是执行一会,中断一下,再执行,一直重复。而对于多个任务,它会先执行高优先级任务。
对于scheduler
的执行特性,可以看出来主要是对两种形式进行优化:多个任务之间的管理和单个任务的执行控制。这也就引申出来scheduler
两种概念:时间片、任务优先级 。
时间片与优先级
时间片是指在单个任务在这一帧内最大的执行时间,超过这个时间后会立即被打断,不会一直占用线程,这样页面就不会因为任务连续执行的时间过长而产生视觉上的卡顿。
优先级是指在有多个任务待执行时,按照优先级的顺序依次执行,这样可以使一些紧急任务先被执行。
既然存在着优先级的概念。那么必然存在一个任务队列对所有的任务进行管理,按照某种顺序对所有的任务进行排序。在scheduler
中存在着两种队列,分别对不同的任务进行管理。
优先级
在探究调度函数是如何加入到队列中之前,先来看一下scheduler
中优先级是如何被划分的:
// 无优先级
export const NoPriority = 0;
// 最高优先级 立即执行
export const ImmediatePriority = 1;
// 用户阻塞级别的优先级
export const UserBlockingPriority = 2;
// 常规优先级
export const NormalPriority = 3;
// 较低优先级
export const LowPriority = 4;
// 空闲优先级 可闲置的任务
export const IdlePriority = 5;
可以看到scheduler
定义了六种优先级,使用数字表示,不同的优先级会参与不同任务过期时间的计算(下文解释)。
任务队列
首先在scheduler
接收一个任务后,会为这个任务创建一个调度对象,对象中保存这个任务的优先级,开始时间,过期时间以及真实的执行函数:
const newTask = {
id: 0,
// 任务函数
callback,
// 任务优先级
priorityLevel,
// 开始时间
startTime,
// 过期时间
expirationTime,
// 在队列中排序的依据
sortIndex: -1,
};
-
sortIndex
:在队列中的排序依据,在scheduler
中使用小顶堆的形式来构建队列 -
callback
:真正的任务函数,也就是外部传入的任务函数 -
priorityLevel
:任务优先级,参与计算任务过期时间 -
startTime
:表示任务开始的时间 -
expirationTime
:表示任务的过期时间
callback
保存真正需要执行的任务函数。priorityLevel
为上文中依据不同的触发类型划分的优先级。startTime
在生成调度对象时被初始化为当前时间:
// getCurrentTime函数获取当前时间
const currentTime = getCurrentTime();
但是过期时间expirationTime
是怎么计算出来的呢?一旦过期时间小于当前时间,则说明当前的任务需要立即被执行,在scheduler
中计算过期时间与优先级密切相关。
不同优先级都会对应的不同的任务过期时间间隔:
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
var USER_BLOCKING_PRIORITY_TIMEOUT = 250;
var NORMAL_PRIORITY_TIMEOUT = 5000;
var LOW_PRIORITY_TIMEOUT = 10000;
// Never times out
var IDLE_PRIORITY_TIMEOUT = maxSigned31BitInt;
ImmediatePriority --> IMMEDIATE_PRIORITY_TIMEOUT --> -1
UserBlockingPriority --> USER_BLOCKING_PRIORITY_TIMEOUT --> 250
NormalPriority --> NORMAL_PRIORITY_TIMEOUT --> 5000
LowPriority --> LOW_PRIORITY_TIMEOUT --> 10000
IdlePriority --> IDLE_PRIORITY_TIMEOUT --> maxSigned31BitInt
而过期时间的计算,则是任务开始时间加上优先级代表的时间间隔:
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;
}
// startTime看作任务开始时间
var expirationTime = startTime + timeout;
ImmediatePriority
的执行间隔为-1,在计算过期时间时当前时间与-1相加,也就是说ImmediatePriority
优先级的任务过期时间肯定会小于任务开始时间。
当调度对象被确定后,会把任务分成了两种:未过期的和已过期的。分别用两个队列存储,前者存到timerQueue
中,后者存到taskQueue
中。
判断任务是否过期使用任务的开始时间startTime
和当前时间currentTime
(当前时间)作比较。开始时间大于当前时间,说明未过期,放到timerQueue
队列;开始时间小于等于当前时间,说明已过期,放到taskQueue
队列。
当任务被放入不同的队列时,两个队列需要按照一定的规则来排序:
-
timerQueue
队列按照startTime
开始时间排序,开始时间越小越靠前。因为开始时间越早,说明会越早开始。任务进来的时候,开始时间默认是当前时间,如果进入调度的时候传了延迟时间,开始时间则是当前时间与延迟时间的和。 -
taskQueue
队列按照expirationTime
过期时间排序。过期时间越早,说明越紧急,过期时间小的排在前面。
如果任务被放到了taskQueue
队列,那么立即调度一个函数去循环taskQueue
,挨个执行里面的任务。
如果任务被放到了timerQueue
队列,那么说明它里面的任务都不会立即执行。等待排在第一位的任务间隔时间到了之后,将第一个任务加入到taskQueue
队列中。然后重复执行这个动作,直到timerQueue
队列中的任务被清空。
任务队列的核心就是保证最紧急的任务优先被调度执行。
scheduleCallback
scheduleCallback
函数是整个调度的入口函数。主要负责生成调度任务、根据任务是否过期将任务放入timerQueue
或taskQueue
,然后触发调度行为,让任务开始进入调度。
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前时间
var currentTime = getCurrentTime();
// 任务开始时间
var startTime;
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; // -1
break;
case UserBlockingPriority:
timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250
break;
case IdlePriority:
timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823
break;
case LowPriority:
timeout = LOW_PRIORITY_TIMEOUT; // 10000
break;
case NormalPriority:
default:
timeout = NORMAL_PRIORITY_TIMEOUT; // 5000
break;
}
// 计算任务的过期时间,任务开始时间 + timeout
// 若是立即执行的优先级(ImmediatePriority)过期时间是startTime - 1,意味着立刻就过期
var expirationTime = startTime + timeout;
// 创建调度任务
var newTask = {
id: taskIdCounter++,
// 真正的任务函数
callback,
// 任务优先级
priorityLevel,
// 任务开始的时间,表示任务何时才能执行
startTime,
// 任务的过期时间
expirationTime,
// 在小顶堆队列中排序的依据
sortIndex: -1,
};
// 如果任务已过期,则将 newTask 放入taskQueue,调用requestHostCallback函数
// 开始调度执行taskQueue中的任务
if (startTime > currentTime) {
// 任务未过期,以开始时间作为timerQueue排序的依据
newTask.sortIndex = startTime;
// 加入timerQueue队列
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 如果现在taskQueue中没有任务,并且当前的任务是timerQueue中排名最靠前的那一个
// 那么需要检查timerQueue中有没有需要放到taskQueue中的任务
if (isHostTimeoutScheduled) {
// 因为即将调度一个requestHostTimeout,所以如果之前已经调度了,那么取消掉
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// 调用requestHostTimeout实现任务的转移,开启调度
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 任务已经过期,以过期时间作为taskQueue排序的依据
newTask.sortIndex = expirationTime;
// 加入taskQueue队列
push(taskQueue, newTask);
// 开始执行任务,使用flushWork去执行taskQueue
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
}
return newTask;
}
首先会为传入的任务计算开始时间,支持用户自定义延迟时间,代表多久之后开始执行,如果没有延迟时间,则将当前时间作为开始时间,代表立即开始执行。
然后根据优先级计算任务的过期时间,若是立即执行的优先级(ImmediatePriority
)过期时间是startTime - 1
,意味着立刻就过期。
根据优先级,开始/过期时间构建调度对象,其中callback
保存真正的任务函数。
根据开始时间为调度函数分配任务队列:
- 开始时间小于当前时间:代表当前任务需要开始调度执行,将它放入
taskQueue
队列中,调用requestHostCallback
函数,让调度者调度一个执行函数去执行任务,也就意味着调度流程开始。 - 开始时间大于当前时间:代表任务还不需要开始调度执行,会将任务放入
timerQueue
队列,并按照开始时间排列,然后调用requestHostTimeout
函数开启一个定时器,设置时间间隔为第一个timerQueue
队列任务的开始时间,再去检查它是否已经到达开始时间,如果到达开始时间startTime
那么加入taskQueue
队列。这个过程通过handleTimeout
函数处理。
requestHostTimeout
函数开启一个定时器。
// 开始一个定时器
function requestHostTimeout(callback, ms,) {
localSetTimeout(() => {
callback(getCurrentTime());
}, ms);
}
handleTimeout
的职责是:
-
调用
advanceTimers
,检查timerQueue
队列中过期的任务,放到taskQueue
中。 -
检查是否已经开始调度,如尚未调度,检查
taskQueue
中是否已经有任务:- 如果有,而且现在是空闲的,说明之前的advanceTimers已经将过期任务放到了taskQueue,那么现在立即开始调度,执行任务
- 如果没有,而且现在是空闲的,说明之前的
advanceTimers
并没有检查到timerQueue
中有过期任务,那么再次调用requestHostTimeout
重复这一过程。
function handleTimeout(currentTime) {
isHostTimeoutScheduled = false;
// 切换任务队列
advanceTimers(currentTime);
if (!isHostCallbackScheduled) {
if (peek(taskQueue) !== null) {
isHostCallbackScheduled = true;
requestHostCallback();
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
其中pop
以及peek
和push
都是操作队列的方法,pop
为队列第一项被弹出(类似于数组的pop()
方法)。
peek
为获取队列的第一项,与pop
方法的区别是,peek
只是获取,不会删除,类似于数组的arr[0]
。
push
为向队列尾部加入一项,类似于数组的push()
方法。
function advanceTimers(currentTime) {
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
pop(timerQueue);
// 任务需要开始了,去taskQueue队列吧
} else if (timer.startTime <= currentTime) {
pop(timerQueue);
// 将sortIndex变更为过期时间
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
} else {
return;
}
timer = peek(timerQueue);
}
}
requestHostCallback
从requestHostCallback
开始正式进入一个任务调度执行的过程了。上文中调用requestHostCallback
函数是这样被调用的:
// 开始执行任务,使用flushWork去执行taskQueue
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
首先将isHostCallbackScheduled
设置为true
,表示当前正在调度一个任务。然后将flushWork
传入requestHostCallback
函数开始执行,那么这个flushWork
是什么呢?
既然flushWork
是入参,那么flushWork
必然是实际执行任务的函数,而requestHostCallback
对任务调度进行调度。
let schedulePerformWorkUntilDeadline;
// node/ie
if (typeof localSetImmediate === 'function') {
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
// 浏览器环境 MessageChannel
} 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);
};
}
function requestHostCallback() {
// 当前是否有正在执行的任务?
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
在不同的宿主环境调用方式是不同的:
- 非浏览器环境,使用定时器执行,因为在非浏览器环境中,用户是看不到界面的,也不存在用户交互,所以卡顿与否也就不那么重要了。
- 浏览器中使用
MessageChannel
实现
schedulePerformWorkUntilDeadline
在不同的宿主环境会被赋予不同的实现方式,我们目前只关注浏览器中的实现方式,先来简单了解一下MessageChannel
是怎么使用的:
MessageChannel
MessageChannel
接口允许我们创建一个新的消息通道,并通过它的两个 MessagePort
属性发送数据。
同时允许我们在不同的浏览上下文,比如window.open()
打开的窗口或者iframe等之间建立通信管道,并通过两端的端口(port1
和port2
)发送消息。MessageChannel
以DOM Event
的形式发送消息,所以它属于异步的宏任务。
port1
订阅一个消息,当port2
发送一个消息时,port1
执行订阅的函数并且接收到了port2
发送的消息。
const { port1, port2 } = new MessageChannel();
port1.onmessage = function (event) {
console.log('收到来自port2的消息:', event.data); // 收到来自port2的消息: pong
};
port2.postMessage('pong');
回到我们scheduler
的执行过程,在浏览器中schedulePerformWorkUntilDeadline
被赋予了触发postMessage
的能力,而接收者port1
订阅了执行函数performWorkUntilDeadline
,所以在schedulePerformWorkUntilDeadline
函数执行的时候相当于port2
通过消息通道发送了一条消息,port1
执行performWorkUntilDeadline
函数,也就是真正任务的执行在performWorkUntilDeadline
函数中。这个过程中会涉及任务的中断和恢复、任务完成状态的判断。
performWorkUntilDeadline
performWorkUntilDeadline
函数的职责是按照时间片的限制去中断任务,并通知调度者再次调度一个新的执行者去继续任务。直到任务被完全执行完。
// 记录开始时间
let startTime = -1;
const performWorkUntilDeadline = () => {
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// 注意此时将任务开始时的当前时间记录下来
// 后面切分时间片使用
startTime = currentTime;
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// 如果还有任务,继续让调度者调度执行者
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
}
};
hasMoreWork
函数保存本次执行的执行结果,是否继续调度任务依赖于flushWork
的执行结果
flushWork
作为真正去执行任务的函数,它会循环taskQueue
队列,逐一调用里面的任务函数。
function flushWork(initialTime: number) {
// ...省略
return workLoop(initialTime);
}
flushWork
函数中将workLoop
函数的执行结果return
出去,任务执行的核心内容看来就在workLoop
中。workLoop
的调用使得任务最终被执行。
在workLoop
函数中,首先就会涉及到任务的中断和恢复。
任务的中断和恢复
scheduler
根据时间片限制任务的执行时间,既然任务会被强制中断,那么也就必然会恢复执行。
先来看一下workLoop
大体的执行流程:
function workLoop(initialTime) {
let currentTime = initialTime;
currentTask = peek(taskQueue);
while (currentTask !== null) {
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// 中断任务
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
// ...省略代码
// 执行任务
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
// 如果currentTask不为空,说明是时间片的限制导致了任务中断
// return 一个 true告诉外部,此时任务还未执行完,任务是被中断
return true;
} else {
// 如果currentTask为空,说明taskQueue队列中的任务已经执行完
// 开始查找timerQueue队列中是否还有任务
const firstTimer = peek(timerQueue);
// 如果timerQueue队列中还有任务,创建一个定时器
// 等待到达任务的开始时间后放入taskQueue队列
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
// return false 告诉外部当前的taskQueue队列已经被清空
return false;
}
可以看到,整个workLoop
函数分为两个部分去执行任务:任务执行和任务状态的判断。
taskQueue循环
当workLoop
开始执行时,使while
循环执行任务,在执行的过程中可以看到时间片的中断条件:
在任务的过期时间大于当前时间时(说明任务还未过期),并且shouldYieldToHost
函数true
。
if (currentTask.expirationTime > currentTime && shouldYieldToHost()) {
// 中断任务
break;
}
shouldYieldToHost
函数用户在任务执行时切分时间片,当任务时间超过规定的时间时,强制中断:
被break
的只是while
循环,while
下部还是会判断currentTask
的状态。
export const frameYieldMs = 5;
let frameInterval = frameYieldMs;
function shouldYieldToHost(): boolean {
// 计算时间间隔
const timeElapsed = getCurrentTime() - startTime;
// 间隔时间大于5毫秒,中断执行
if (timeElapsed < frameInterval) {
return false;
}
return true
}
performWorkUntilDeadline
在开始任务执行的时候记录开始时间,在任务每次执行过程中(while
循环),判断任务开始的时间与当前时间是否大于5ms,以此对任务进行切分。
currentTask
正常执行,可是时间不允许,那只能先break
掉本次while
循环,使得本次循环currentTask
执行的逻辑都不能被执行到。
由于任务只是被中止,所以currentTask
不可能是null
,那么会return
一个true
告诉外部任务并没有执行结束,只是被暂时中断。
否则说明全部的任务都已经执行完了,taskQueue
任务队列已经被清空了,此时return
一个false
让外部终止本次调度。
由于workLoop
的执行结果会被flushWork
return
出去,所以在performWorkUntilDeadline
函数中整个执行流程就会非常清晰:
const performWorkUntilDeadline = () => {
if (isMessageLoopRunning) {
// 获取当前时间
const currentTime = getCurrentTime();
startTime = currentTime;
let hasMoreWork = true;
try {
// 当任务因为时间片被打断时,它会返回true,表示还有任务
hasMoreWork = flushWork(currentTime);
} finally {
// hasMoreWork为true,代表人物只是被中断了,并没有执行完
if (hasMoreWork) {
// 调度一个调度者,继续去执行完任务
schedulePerformWorkUntilDeadline();
} else {
// 如果没有任务了,停止调度
isMessageLoopRunning = false;
}
}
}
needsPaint = false;
};
当任务被打断之后,performWorkUntilDeadline
会再让调度者调用一个执行者,继续执行这个任务,直到任务完成。
由于任务被中断只是循环被break
,当前被中断的任务还依然保存在taskQueue
队列中的第一个并未被弹出,所以下次调度执行的任务依然是上次被中断的那一个。
所以整个任务的执行流程:
调度任务 --> 执行任务 --> 打断 --> 调度任务 --> 执行任务 ... --> 完成
既然中断和恢复任务的流程已经明白是如何运行的了,那么如何判断该任务是否完成呢?
如何判断任务的完成状态?
任务的中断恢复是一个重复的过程,该过程会一直重复到任务完成。所以判断任务是否完成非常重要,而任务未完成则会重复执行任务函数。
由于scheduler
是完全独立的一个包,调用的时候将需要调度的任务交给它,由它来调度执行,但是scheduler
在调度的时候只是关注于去执行任务,触及不到具体的任务的执行逻辑,所以在调度的时候它是无法准确的得知任务是什么时候结束的,它可以做到重复执行任务函数,但边界(即任务是否完成)却无法像递归那样直接获取,只能依赖任务函数的返回值去判断。
所以若执行的任务函数返回值为函数,那么就说明当前任务尚未完成,需要继续调用任务函数,否则任务完成。workLoop
就是通过判断返回值的方式判断单个任务的完成状态。
(以下实现有删减):
function workLoop(initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
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);
currentTime = getCurrentTime();
// 如果任务只是被中断了,返回值为一个函数,任务函数自身
if (typeof continuationCallback === 'function') {
// 检查callback的执行结果返回的是不是函数,如果返回的是函数,则将这个函数作为当前任务新的回调。
currentTask.callback = continuationCallback;
return true;
} else {
// 如果返回值不是函数,说明任务已经彻底执行完毕
if (currentTask === peek(taskQueue)) {
// 在taskQueue队列中弹出当前任务
pop(taskQueue);
}
}
// 查找timerQueue队列中是否有可以开始执行的任务
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// 返回当前任务是否被执行完的标记
// 如果执行完 (else逻辑)则需要去timerQueue中查看是否有需要加入到taskQueue的任务
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
所以,workLoop是通过判断任务函数的返回值去识别任务的完成状态的。
开始调度后,调度者调度执行者去执行任务,实际上是执行任务上的callback
(也就是任务函数)。如果执行者判断callback
返回值为一个function
,说明任务并没有完成,只是暂时被中断。
那么会将返回的这个function
再次赋值给任务的callback
,由于任务还未完成,所以并不会被弹出出taskQueue
队列,currentTask
获取到的还是这个任务,while
循环到下一次还是会继续执行这个任务,直到任务完成弹出队列,才会继续调度下一个任务。
取消任务
取消任务的场景是如果当前正在执行一个比较低优先级的任务,这是有一个高优先级的任务进来了,所以就需要先将正在执行的低优先级的任务取消掉,然后调度一个任务去优先执行高优先级的任务。
在workLoop
中获取当前调度对象的callback
属性,如果是函数就开始执行,如果不是的话直接将这个任务弹出taskQueue
队列。
function workLoop(initialTime) {
...
// 获取taskQueue中最紧急的任务
currentTask = peek(taskQueue);
while (currentTask !== null) {
...
const callback = currentTask.callback;
if (typeof callback === 'function') {
// 执行任务
} else {
// 如果callback为null,将任务出队
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
...
}
所以取消任务直接将callback
属性赋值为null
就可以了。
function unstable_cancelCallback(task) {
// ...省略
task.callback = null;
}
workLoop
中,如果callback
是null
会被移出taskQueue
队列,所以当前的这个任务就不会再被执行了。它取消的是当前任务的执行,while
循环还会继续执行下一个任务。
Demo
最后实现一个依靠recheduler
来调度任务生成数据的小案例:
import {
unstable_ImmediatePriority as ImmediatePriority,
unstable_UserBlockingPriority as UserBlockingPriority,
unstable_NormalPriority as NormalPriority,
unstable_LowPriority as LowPriority,
unstable_IdlePriority as IdlePriority,
unstable_scheduleCallback as scheduleCallback,
unstable_shouldYield as shouldYield,
CallbackNode,
unstable_getFirstCallbackNode as getFirstCallbackNode,
unstable_cancelCallback as cancelCallback
} from 'scheduler';
import './style.css';
const button = document.querySelector('button');
const root = document.querySelector('#root');
type Priority =
| typeof IdlePriority
| typeof LowPriority
| typeof NormalPriority
| typeof UserBlockingPriority
| typeof ImmediatePriority;
interface Work {
count: number;
priority: Priority;
}
const workList: Work[] = [];
let prevPriority: Priority = IdlePriority;
let curCallback: CallbackNode | null = null;
[LowPriority, NormalPriority, UserBlockingPriority, ImmediatePriority].forEach(
(priority) => {
const btn = document.createElement('button');
root?.appendChild(btn);
btn.innerText = [
'',
'ImmediatePriority',
'UserBlockingPriority',
'NormalPriority',
'LowPriority'
][priority];
btn.onclick = () => {
workList.unshift({
count: 100,
priority: priority as Priority
});
schedule();
};
}
);
function schedule() {
const cbNode = getFirstCallbackNode();
const curWork = workList.sort((w1, w2) => w1.priority - w2.priority)[0];
// 策略逻辑
if (!curWork) {
curCallback = null;
cbNode && cancelCallback(cbNode);
return;
}
const { priority: curPriority } = curWork;
if (curPriority === prevPriority) {
return;
}
// 更高优先级的work
cbNode && cancelCallback(cbNode);
curCallback = scheduleCallback(curPriority, perform.bind(null, curWork));
}
function perform(work: Work, didTimeout?: boolean) {
/**
* 1. work.priority
* 3. 时间切片
*/
const needSync = work.priority === ImmediatePriority || didTimeout;
while ((needSync || !shouldYield()) && work.count) {
work.count--;
insertSpan(work.priority + '');
}
// 中断执行 || 执行完
prevPriority = work.priority;
if (!work.count) {
const workIndex = workList.indexOf(work);
workList.splice(workIndex, 1);
prevPriority = IdlePriority;
}
const prevCallback = curCallback;
schedule();
const newCallback = curCallback;
if (newCallback && prevCallback === newCallback) {
return perform.bind(null, work);
}
}
function insertSpan(content) {
const span = document.createElement('span');
span.innerText = content;
span.className = `pri-${content}`;
doSomeBuzyWork(10000000);
root?.appendChild(span);
}
function doSomeBuzyWork(len: number) {
let result = 0;
while (len--) {
result += len;
}
}
那么扯了这么多,等下次面试再遇到这个问题怎么回答呢?
总结
scheduler
管理taskQueue
和timerQueue
两个队列,定期将timerQueue
中的到达开始时间的任务放到taskQueue
中,然后让调度者通知执行者循环taskQueue
执行每一个任务。
执行者控制着每个任务的执行,一旦某个任务的执行时间超出时间片的限制。就会被中断,然后当前的执行者退出,退出之前会通知调度者再去调度一个新的执行者继续完成这个任务,新的执行者在执行任务时依旧会根据时间片中断任务,然后退出,重复这一过程,直到当前这个任务彻底完成后,将任务从taskQueue
出队。
taskQueue
中每一个任务都被这样处理,最终完成所有任务,这就是scheduler
的完整工作流程。
写在最后 ⛳
未来可能会更新实现mini-react
和antd
源码解析系列,希望能一直坚持下去,期待多多点赞🤗🤗,一起进步!🥳🥳
转载自:https://juejin.cn/post/7331135154209308687