目录
2021SC@SDUSC
react-reconciler与scheduler关系
这里给出react-reconciler与scheduler的关系图
关于react17的最新版本,我查阅多方资料,但由于在任务调度方面好像改动不小,所以不得不自己参考源码,最终找到了react与scheduler的连通入口,在ReactFiberWorkLoop.new.js中的schedulerCallBack正是这样的方法,它来自于scheduler.js中。根据该方法找到scheduler.js的源码,接下来开始介绍sceduler。
scheduler介绍
作为调度模块,可以首先看一下任务优先级
对应scheduler.js中的scheduler方法的是源码中的 unstable_scheduleCallback方法。
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;
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,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) {
// This is a delayed task.
newTask.sortIndex = startTime;
push(timerQueue, newTask);
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// All tasks are delayed, and this is the task with the earliest delay.
if (isHostTimeoutScheduled) {
// Cancel an existing timeout.
cancelHostTimeout();
} else {
isHostTimeoutScheduled = true;
}
// Schedule a timeout.
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
newTask.sortIndex = expirationTime;
push(taskQueue, newTask);
if (enableProfiling) {
markTaskStart(newTask, currentTime);
newTask.isQueued = true;
}
// Schedule a host callback, if needed. If we're already performing work,
// wait until the next time we yield.
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
接下来对该方法进行分析,该方法需要传入优先级,(delay是options可选项中的属性,callback的开始时间就是当前时间+delay),然后根据优先级计算出timeout,根据不同的优先级,对应不同的timeout常量,而过期时间就是开始时间+timeout,接着进行比较,如果开始时间小于当前时间,就将其放入timerQueue中然后等待直到taskQueue中的任务执行完为止,通过handleTimeout设立等待时间,当等待时间满足就将其放入taskQueue中,(由图中代码可知,new task 的ID应当为taskIDcounter++)如果开始时间大于当前时间,就直接将其放入taskqueue中(按照过期时间的顺序进行排序)。当前无任务执行时就会执行requestHostCallback以对任务进行调度。在requestHostCallback中会将flushWork赋值给scheduledHostCallback,也就是说,执行任务的函数为flushWork。当前isMessageLoopRunning为false,也就是messageChannel尚未运行,那就执行schedulePerformWorkUntilDeadline,该函数向messageChannel的port2端口发送信息(此时performWorkUntilDeadline已经在port1处监听),所以执行 performWorkUntilDeadline方法,在performWorkUntilDeadline方法中会执行scheduledHostCallback,也就是flushWork,逐步执行完毕taskQueue中的任务。
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
const hasTimeRemaining = true;
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
//
// Intentionally not using a try-catch, since that makes some debugging
// techniques harder. Instead, if `scheduledHostCallback` errors, then
// `hasMoreWork` will remain true, and we'll continue the work loop.
let hasMoreWork = true;
try {
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
在该方法中获得当前时间,并设置hasTimeRemain为True并执行 scheduledHostCallback 也就是flushWork方法,根据该方法确定是否有更多任务,也就是提到的hasMoreWork,如果有,继续通过schedulePerformWorkUntilDeadline方法调度,如果没有就退出。而在flushWork中会执行workLoop方法,workLoop方法调用时会执行任务。
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime;
advanceTimers(currentTime);
currentTask = peek(taskQueue);
while (
currentTask !== null &&
!(enableSchedulerDebugging && isSchedulerPaused)
) {
if (
currentTask.expirationTime > currentTime &&
(!hasTimeRemaining || shouldYieldToHost())
) {
// This currentTask hasn't expired, and we've reached the deadline.
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
if (enableProfiling) {
markTaskRun(currentTask, currentTime);
}
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
if (typeof continuationCallback === 'function') {
currentTask.callback = continuationCallback;
if (enableProfiling) {
markTaskYield(currentTask, currentTime);
}
} else {
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime);
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue);
}
// Return whether there's additional work
if (currentTask !== null) {
return true;
} else {
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
在workLoop方法中会得到taskQueue中最需要执行的任务然后执行,由于是分时间片执行,在执行过程中会在每个时间片执行一次任务函数,并且反复检测任务函数的返回值,假如返回值为函数,说明尚未完成,那么循环执行,假如返回值为null,说明该任务函数已完成,那就在taskQueue中寻找下一个任务继续执行。而workLoop循环退出有两种可能,没有剩余时间分给时间片或者没有下一个任务,假如此时hasTimeRemaining为false,说明没有剩余时间,就需要退出,但保留当前任务,那么此时workLoop的返回值为true,代表hasmorework,那就继续通过messageChannel进行调度。假如currentTask为null,说明taskQueue中任务已完成,已经没有下一个任务,那就就执行requestHostTimeout方法等待从timerQueue中获取任务放入taskQueue中并且返回false,代表没有更多工作,调度已经完成。这是scheduler中比较重要的部分。
JS中的偏函数
这里说一下JS中的偏函数,由bind实现,可以进行预定义参数,因为在workLoop中只会传入didUserCallbackTimeout这一布尔值,所以需要预先绑定root。
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
另外,在ensureRootIsScheduled方法中可以检查任务,如果出现新的较高优先级的任务,就会使用执行cancelCallback,将当前任务的callback设置为null,从而使workLoop将正在执行的任务送出taskQueue,造成中断,实现优先级调度。
总结
介绍了react-reconciler与scheduler的关系,分析了react中如何通过scheduler实现对多任务的优先级调度以及在scheduler的workLoop中单个任务是否完成的判断,还补充解释了JS中关于偏函数的知识。