var channel = new MessageChannel();
var port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = function () {
port.postMessage(null);
};
var performWorkUntilDeadline = function () {
/**
* 这里的 scheduledHostCallback 其实就是 flushWork
* 详细见 requestHostCallback
*/
if (scheduledHostCallback !== null) {
var currentTime = exports.unstable_now();
startTime = currentTime;
var hasTimeRemaining = true;
var hasMoreWork = true;
try {
/**
* 执行 flushWork,其返回值代表着 taskQ 是否为空
* 如果不为空,则代表着仍然存在着需要被调度的 task
*/
hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime);
} finally {
/**
* 如果
*/
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
/**
* 因为仍然存在需要调度的任务,便再次触发 MessageChannel
*/
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
scheduledHostCallback = null;
}
}
} else {
isMessageLoopRunning = false;
} // Yielding to the browser will give it a chance to paint, so we can
};
function shouldYieldToHost() {
/**
* performWorkUntilDeadline 在执行起始时就会给 startTime 赋值
*/
const timeElapsed = getCurrentTime() - startTime;
/**
* frameInterval 默认值是 5 ms
* 不过也会根据显示器的 fps(0, 125) 自动计算,计算方式为 Math.floor(1000 / fps)
* 也就是说一帧的毫秒数,而这个值就是时间切片
*/
if (timeElapsed < frameInterval) {
// The main thread has only been blocked for a really short amount of time;
// smaller than a single frame. Don't yield yet.
// 因为时间有的多,所以不用中断
return false;
}
// The main thread has been blocked for a non-negligible amount of time. We
// may want to yield control of the main thread, so the browser can perform
// high priority tasks. The main ones are painting and user input. If there's
// a pending paint or a pending input, then we should yield. But if there's
// neither, then we can yield less often while remaining responsive. We'll
// eventually yield regardless, since there could be a pending paint that
// wasn't accompanied by a call to `requestPaint`, or other main thread tasks
// like network events.
/**
* 太长不看:我们要优先响应用户输入 balabala
*/
if (enableIsInputPending) {
if (needsPaint) {
// There's a pending paint (signaled by `requestPaint`). Yield now.
return true;
}
if (timeElapsed < continuousInputInterval) {
// We haven't blocked the thread for that long. Only yield if there's a
// pending discrete input (e.g. click). It's OK if there's pending
// continuous input (e.g. mouseover).
if (isInputPending !== null) {
return isInputPending();
}
} else if (timeElapsed < maxInterval) {
// Yield if there's either a pending discrete or continuous input.
if (isInputPending !== null) {
return isInputPending(continuousOptions);
}
} else {
// We've blocked the thread for a long time. Even if there's no pending
// input, there may be some other scheduled work that we don't know about,
// like a network event. Yield now.
return true;
}
}
// `isInputPending` isn't available. Yield now.
// isInputPending不管事,那么中断算了
return true;
}
React Scheduler: Scheduler 源码分析
TLNR
全文就是在画下面这个图
说明
Scheduler 作为 react 内任务调度的核心是源码阅读绕不开的点(时间切片这一概念其实就存在于该包内),不过幸运的是,该模块是独立的一个包(可脱离 react 内的一些概念,因此逻辑十分干净),并且在打包后也只有小700行左右,因此阅读难度并不算大。
在 Scheduler 内存在两个概念,这里提一下
接下来本文将从 unstable_scheduleCallback 这一函数开始对源码进行分析,该函数可以被视为 Scheduler 的入口函数。
unstable_scheduleCallback
Scheduler 通过暴露该函数给外界以提供在 Scheduler 内注册 task 的能力。阅读该函数需要关注以下几点
至此,根据上述逻辑我们可以绘制出下图
接下来稍微看下 handleTimeout 里面具体做了什么
入口相关的部分便分析结束,从上面的代码中不难注意到 requestHostCallback(flushWork) 这一个代码片段,而它也将成为我们分析调度行为的一个入口。
requestHostCallback
requestHostCallback 可以说是 Scheduler 内触发调度行为的一个入口,因此对它的解析也是分析 Scheduler 的重点之一。接下来我们来看一下它的实现。
上面代码段内不难看出 schedulePerformWorkUntilDeadline 是其关键函数,然而其声明区分了三种场景
不过本文只关注分析浏览器环境,下面是其声明。
MessageChannel 请自行看 MDN,这里只强调一点 MessageChannel 触发的异步任务类型为 MacroTask,因此大多数情况下在该任务执行后总是会触发浏览器的 render。
由上述代码,每次调用 schedulePerformWorkUntilDeadline 都会触发 performWorkUntilDeadline,那么解下来看看这个函数里面是什么
Scheduler 的示意图在此时其实已经将大致框架画出来了。
接下来需要补全 performWorkUntilDeadline 的内容,在接下来的分析过程中,我们很快就会讲到时间切片相关的内容
flushWork 与 workLoop
performWorkUntilDeadline 内会调用 scheduledHostCallback,而 scheduledHostCallback 不过是 flushWork 的别名。(见 requestHostCallback) 但 flushWork 内其实也只需要关注 workLoop 就行了,在 workLoop 内会涉及到时间切片与中断恢复这两个核心概念
经过上方的分析,可以画出 performWorkUntilDeadline 内的大致操作
接下来我们来看看这个时间切片是个什么东西。。
时间切片看着这名字牛逼哄哄,其实就是显示器一帧耗时,然而为什么要设计这么个东西,其实道理也很简单,如果 渲染进程 的主线程 一直被 JS 线程 给占用,而 GUI 线程 无法介入,那么页面便会一直不刷新从而帧数下降,让用户感到卡顿。因此一旦执行任务的耗时超过了时间切片就需要立刻中断任务从而让浏览器刷新页面。