乐趣区

scheduler 源码

为什么是要有 scheduler
首先要从 js 的是单线程模型来说起,Javascript 执行是会经历静态编译,动态解释和事件循环做任务调度的过程,大致的流程如下 (注意,该流程是以 chrome 浏览器内核为标准的执行流程,在 node 或者其他浏览器中,执行流程会有所差异,但是核心思想是差不多的。从这里面我们很直观的认识到 js 的线程模型是怎么工作的。那跟 scheduler 有什么关系呢, 我们都知道 React16 采用了 fiber 架构,这样的架构下,使用链表结构代替原有的函数嵌套,避免无法控制组件渲染的过程的问题,Fiber 让 React 内部会动态灵活的管理所有组件的渲染任务,可以中断暂停某一个组件的渲染,所以,对于复杂型应用来说,对于某一个交互动作的反馈型任务,我们是可以对其进行拆解,一步步的做交互反馈,避免在一个页面重绘时间周期内做过多的事情,这样就能减少应用的长任务,最大化提升应用操作性能,更好的利用有限的时间,那么,我们现在可以只聚焦在任务管理上,一起来研究一下 React 到底是如何调度组件的任务执行的,这里说渲染感觉不太准确
macrotask: setTimeout, setInterval, setImmediate,MessageChannel, I/O, UI rendering netWorkmicrotask: process.nextTick, Promises, Object.observe(废弃), MutationObserver
[引自大神论述]

上图来自知乎文章

主线程负责解析编译和调度异步事件循环调度
异步队列和 V8 通讯 通过 polling Check 来实现
异步队列分成 macotask 和 microtask

macrotask
一般情况下,在没有特别说明的情况下我们会把 macrotask 称为 task queues , 在一次的事件循环中,他只会执行一次
microtask

在每一次事件循环中,macrotask 只会提取一个执行,而 microtask 会一直提取,直到 microtasks 队列清空。
那么也就是说如果我的某个 microtask 任务又推入了一个任务进入 microtasks 队列,那么在主线程完成该任务之后,仍然会继续运行 microtasks 任务直到任务队列耗尽
而事件循环每次只会入栈一个 macrotask,主线程执行完该任务后又会先检查 microtasks 队列并完成里面的所有任务后再执行 macrotask

console.log(‘start’);
setTimeout(function() {
console.log(‘macrotask’);
}, 0);
Promise.resolve().then(function() {
console.log(‘microtask’);
}).then(function() {
console.log(‘microtask’);
});
console.log(‘ end’);
根据上述理论自己试试程序的运行结果, 为什么我们在分析 scheduler 源码之前先要介绍下异步队列,因为了解清楚 js 异步队列才会让我们更加清晰知道 scheduler 是怎么使用调度方法来更好的安排代码执行时机。
浏览器的执行页面绘制过程
图片出自同一地方执行 JS(具体流程在上面有描述)—> 计算 Style—> 构建布局模型 (Layout)—> 绘制图层样式 (Paint)—> 组合计算渲染呈现结果 (Composite)

一个完成的过程称之为一帧
一般设备的刷新频率是 60Hz (还有更大的 scheduler 最大设定为 120hz) 也就是按照理想情况来说一秒钟有 60 帧 那么一帧的平均时间是 1000 / 60 大约是 16.7ms 也就是我们一帧的执行时间不能超过 16.7 否则就会出现丢失帧和卡顿情况
帧的渲染是在处理完流程之后进行的
帧的渲染是在独立的 UI 线程去执行 是有 GPU 等
剩余的时间为空闲时间
在离散型 交互动作中不一定要求需要 16.7 ms 的时间
对于离散型交互,上一帧的渲染到下一帧的渲染时间是属于系统空闲时间,经过亲测,Input 输入,最快的单字符输入时间平均是 33ms(通过持续按同一个键来触发),相当于,上一帧到下一帧中间会存在大于 16.4ms 的空闲时间,就是说任何离散型交互,最小的系统空闲时间也有 16.4ms,也就是说,离散型交互的最短帧长一般是 33ms

requestIdleCallback
在帧的渲染中当执行完流程和 UI 绘制之后 会有一部分空闲时间,如果我们能掌握这个时间加一充分利用就更加理想那如何知道一帧进入这个空闲时间呢,浏览器目前提供了这个回调 requestIdleCallback 即浏览器空闲时
var handle = window.requestIdleCallback(callback[, options]);

requestIdleCallback 回调调用时机是在回调注册完成的上一帧渲染到下一帧渲染之间的空闲时间执行
callback 是要执行的回调函数,会传入 deadline 对象作为参数,deadline 包含:
timeRemaining:剩余时间,单位 ms,指的是该帧剩余时间。
didTimeout:布尔型,true 表示该帧里面没有执行回调,超时了。
option: {timeout:即超时时间,不提供浏览器自己去计算

}
如果给定 timeout,那到了时间,不管有没有剩余时间,都会立刻执行回调 callback。
requestAnimationFrame

以前我们知道:requestAnimationFrame 回调只会在当前页面激活状态下执行,可以大大节省 CPU 开销
requestAnimationFrame 回调参数是回调被调用的时间,也就是当前帧的起始时间(可以通过这个时间去判断 到底有么有超时)
系统控制回调的执行时机恰好在回调注册完成后的下一帧渲染周期的起点的开始执行,控制 js 计算的到屏幕响应的精确性,避免步调不一致而导致丢帧

目前浏览器对于 requestIdleCallback 的支持不是特别完整,所以 react 团队放弃了 requestIdleCallback 的使用自己用 requestAnimationFrame 和 MessageChannel 来 polyfill
requestIdleCallback Polyfill 方案
很简单,33 毫秒,但是时间并不总是 33ms,这个时间是 React 认为的一个可以接受的最大值,如果运行设备能做到大于 30fps,那么它会去调整这个值(通常情况下可以调整到 16.6ms)。调整策略是用当前每帧的总时间与实际每帧的时间进行比较,当实际时间小于当前时间且稳定(前后两次都小于当前时间),那么就会认为这个值是有效的,然后将每帧时间调整为该值(取前后两次中时间大的值),还有就是 requestAnimationFrame 回调的第一个参数,每一帧的起始时间,最终借助 requestAnimationFrame 让一批扁平的任务恰好控制在一块一块的 33ms 这样的时间片内执行即可

所有准备工作都做好了,接下来我们逐步来分析 Scheduler 源码
1、调度基本常量定义
// 枚举

// 立即执行的任务
var ImmediatePriority = 1;

// 用户阻塞优先级
var UserBlockingPriority = 2;

// 一般的优先级
var NormalPriority = 3;

// 低级的优先级
var LowPriority = 4;

// 空闲的优先级
var IdlePriority = 5;

// 我们可以理解 优先级越高 过期时间就越短 反之 越长

// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) – 1
// 0b111111111111111111111111111111
// 最大整数
var maxSigned31BitInt = 1073741823;

// Times out immediately
// 超时的优先级时间 说明没有剩余时间了 需要立即被调度
var IMMEDIATE_PRIORITY_TIMEOUT = -1;
// Eventually times out
// 事件的过期时间 250 ms
var USER_BLOCKING_PRIORITY = 250;

// 一般优先级的过期时间 5000 ms
var NORMAL_PRIORITY_TIMEOUT = 5000;

// 优先级低的 10000ms
var LOW_PRIORITY_TIMEOUT = 10000;

// Never times out 空闲的任务 有没有限制了 也就是最大整数
var IDLE_PRIORITY = maxSigned31BitInt;

2、调度所需变量的定义
// Callbacks are stored as a circular, doubly linked list.
// 回调保存为了循环的双向链表
var firstCallbackNode = null;

// 当前是否过期
var currentDidTimeout = false;
// Pausing the scheduler is useful for debugging.

// 调度是否中断
var isSchedulerPaused = false;

// 默认当前的优先级为一般优先级
var currentPriorityLevel = NormalPriority;

// 当前时间开始时间
var currentEventStartTime = -1;

// 当前过期时间
var currentExpirationTime = -1;

// This is set when a callback is being executed, to prevent re-entrancy.
// 当前是否执行 callback 调度
var isExecutingCallback = false;

// 是否有回调呗调度
var isHostCallbackScheduled = false;

// 支持 performance.now 函数
var hasNativePerformanceNow = typeof performance === ‘object’ && typeof performance.now === ‘function’;
3、调度方法 unstable_scheduleCallback

function unstable_scheduleCallback(callback, deprecated_options) {

// 开始时间
var startTime = currentEventStartTime !== -1 ? currentEventStartTime : exports.unstable_now();

var expirationTime;

// 过期时间 这里模拟的是 requestIdleCallback options 的 timeout 的定义
// 如果这里指定了 timeout 就会计算出 过期时间
// 如果么有指定就会根据 调度程序的优先级去计算 比如 普通是 5000 低级是 10000 空闲就永远不会过期等 ….
if (typeof deprecated_options === ‘object’ && deprecated_options !== null && typeof deprecated_options.timeout === ‘number’) {
// FIXME: Remove this branch once we lift expiration times out of React.
expirationTime = startTime + deprecated_options.timeout;
} else {
switch (currentPriorityLevel) {
case ImmediatePriority:
expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;
break;
case UserBlockingPriority:
expirationTime = startTime + USER_BLOCKING_PRIORITY;
break;
case IdlePriority:
expirationTime = startTime + IDLE_PRIORITY;
break;
case LowPriority:
expirationTime = startTime + LOW_PRIORITY_TIMEOUT;
break;
case NormalPriority:
default:
expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;
}
}

// 新建一个节点
var newNode = {
callback: callback,
priorityLevel: currentPriorityLevel,
expirationTime: expirationTime,
next: null,
previous: null
};

// Insert the new callback into the list, ordered first by expiration, then
// by insertion. So the new callback is inserted any other callback with
// equal expiration.

// 将新回调插入列表,首先按到期排序,然后按插入排序。所以新的回调插入到任何 callback 都拥有相同的过期时间

// 如果链表是空的 则 重新构建
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = newNode.next = newNode.previous = newNode;
ensureHostCallbackIsScheduled();
} else {
var next = null;
var node = firstCallbackNode;

// 先将链表根据过期时间进行排序 遍历查找 寻找比当前过期时间大的节点
do {
if (node.expirationTime > expirationTime) {
// The new callback expires before this one.
next = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);

// 没有找到比当前更靠后的 元素 说明当前的节点是最不优先的
if (next === null) {
// No callback with a later expiration was found, which means the new
// callback has the latest expiration in the list.
// 当前新加入的节点是最后面的 指针指向链表头
next = firstCallbackNode;
} else if (next === firstCallbackNode) {
// The new callback has the earliest expiration in the entire list.

// 说明所有的任务
firstCallbackNode = newNode;
ensureHostCallbackIsScheduled();
}

// 将新的 node 加入到链表 维护一下循环链表
var previous = next.previous;
previous.next = next.previous = newNode;
newNode.next = next;
newNode.previous = previous;
}

return newNode;
}
4. ensureHostCallbackIsScheduled 遇到优先级高的 需要特别处理
function ensureHostCallbackIsScheduled() {

// 调度正在执行 返回 也就是不能打断已经在执行的
if (isExecutingCallback) {
// Don’t schedule work yet; wait until the next time we yield.
return;
}
// Schedule the host callback using the earliest expiration in the list.
// 让优先级最高的 进行调度 如果存在已经在调度的 直接取消
var expirationTime = firstCallbackNode.expirationTime;
if (!isHostCallbackScheduled) {
isHostCallbackScheduled = true;
} else {
// Cancel the existing host callback.
// 取消正在调度的 callback
cancelHostCallback();
}
// 发起调度
requestHostCallback(flushWork, expirationTime);
}

5、requestAnimationFrameWithTimeout
var localSetTimeout = typeof setTimeout === ‘function’ ? setTimeout : undefined;
var localClearTimeout = typeof clearTimeout === ‘function’ ? clearTimeout : undefined;

// We don’t expect either of these to necessarily be defined, but we will error
// later if they are missing on the client.
var localRequestAnimationFrame = typeof requestAnimationFrame === ‘function’ ? requestAnimationFrame : undefined;
var localCancelAnimationFrame = typeof cancelAnimationFrame === ‘function’ ? cancelAnimationFrame : undefined;

// requestAnimationFrame does not run when the tab is in the background. If
// we’re backgrounded we prefer for that work to happen so that the page
// continues to load in the background. So we also schedule a ‘setTimeout’ as
// a fallback.
// TODO: Need a better heuristic for backgrounded work.
var ANIMATION_FRAME_TIMEOUT = 100;
var rAFID;
var rAFTimeoutID;
/**
* 是解决网页选项卡如果在未激活状态下 requestAnimationFrame 不会被触发的问题,
* 这样的话,调度器是可以在后台继续做调度的,一方面也能提升用户体验,
* 同时后台执行的时间间隔是以 100ms 为步长,这个是一个最佳实践,100ms 是不会影响用户体验同时也不影响 CPU 能耗的一个折中时间间隔
为什么要用 settimeout 因为 requestAnimationFrame 不会在 tab 不激活的情况下不执行
*/
var requestAnimationFrameWithTimeout = function (callback) {
// schedule rAF and also a setTimeout
rAFID = localRequestAnimationFrame(function (timestamp) {
// cancel the setTimeout
localClearTimeout(rAFTimeoutID);
callback(timestamp);
});
rAFTimeoutID = localSetTimeout(function () {
// cancel the requestAnimationFrame
localCancelAnimationFrame(rAFID);
callback(exports.unstable_now());
}, ANIMATION_FRAME_TIMEOUT);
};

if (hasNativePerformanceNow) {
var Performance = performance;
exports.unstable_now = function () {
return Performance.now();
};
} else {
exports.unstable_now = function () {
return localDate.now();
};
}
6、调度 requestHostCallback
这里 react 做了特别的兼容处理 注入方式和不支持 window 或者 MessageChannel 的方式 这里不做主要分析 因为比较简单,这里将主要研究现代浏览器的处理方式
var requestHostCallback;
var cancelHostCallback;
var shouldYieldToHost;

var globalValue = null;
if (typeof window !== ‘undefined’) {
globalValue = window;
} else if (typeof global !== ‘undefined’) {
globalValue = global;
}

if (globalValue && globalValue._schedMock) {
// Dynamic injection, only for testing purposes.
// 动态注入 用于测试目的
var globalImpl = globalValue._schedMock;
requestHostCallback = globalImpl[0];
cancelHostCallback = globalImpl[1];
shouldYieldToHost = globalImpl[2];
exports.unstable_now = globalImpl[3];
} else if (

// 非 DOM 环境
// If Scheduler runs in a non-DOM environment, it falls back to a naive
// implementation using setTimeout.
typeof window === ‘undefined’ ||
// Check if MessageChannel is supported, too.
typeof MessageChannel !== ‘function’) {
// If this accidentally gets imported in a non-browser environment, e.g. JavaScriptCore,
// fallback to a naive implementation.
var _callback = null;
var _flushCallback = function (didTimeout) {
if (_callback !== null) {
try {
_callback(didTimeout);
} finally {
_callback = null;
}
}
};
requestHostCallback = function (cb, ms) {

// 这里的调度就直接在 settimeout 去执行 也就是直接放入 macrotask 队列 这里应该是下下策
if (_callback !== null) {
// Protect against re-entrancy.
setTimeout(requestHostCallback, 0, cb);
} else {
_callback = cb;
setTimeout(_flushCallback, 0, false);
}
};
cancelHostCallback = function () {
_callback = null;
};
shouldYieldToHost = function () {
return false;
};
} else {
if (typeof console !== ‘undefined’) {
// TODO: Remove fb.me link
if (typeof localRequestAnimationFrame !== ‘function’) {
console.error(“This browser doesn’t support requestAnimationFrame. ” + ‘Make sure that you load a ‘ + ‘polyfill in older browsers. https://fb.me/react-polyfills’);
}
if (typeof localCancelAnimationFrame !== ‘function’) {
console.error(“This browser doesn’t support cancelAnimationFrame. ” + ‘Make sure that you load a ‘ + ‘polyfill in older browsers. https://fb.me/react-polyfills’);
}
}

// 调度的 callback
var scheduledHostCallback = null;
// 消息发送中标识
var isMessageEventScheduled = false;
// 过期时间
var timeoutTime = -1;
// rAF 轮询启动状态
var isAnimationFrameScheduled = false;

// 任务执行中标识
var isFlushingHostCallback = false;
// 下一帧期望完成时间点,用于判断重绘后 js 线程是否空闲,还是长期占用
var frameDeadline = 0;
// We start out assuming that we run at 30fps but then the heuristic tracking
// will adjust this value to a faster fps if we get more frequent animation
// frames.
/**
*
我们假设我们以 30fps 运行,然后进行启发式跟踪
如果我们获得更频繁的动画,我会将此值调整为更快的 fps

默认 33 为什么是 33 因为我们假定每秒 30 帧固定评率刷新 也就是 一帧需要 33ms
*/
var previousFrameTime = 33;
var activeFrameTime = 33;

// 以此推断线程是否空闲,好添加并处理新任
shouldYieldToHost = function () {
return frameDeadline <= exports.unstable_now();
};

// We use the postMessage trick to defer idle work until after the repaint.
// 使用 postMessage 来跟踪判断重绘是否完成
var channel = new MessageChannel();
var port = channel.port2;

// 当 port1 发送消息后 这里在帧重绘完成后 进入 message 回调 接着处理我们
// callback
channel.port1.onmessage = function (event) {

isMessageEventScheduled = false;
var prevScheduledCallback = scheduledHostCallback;
var prevTimeoutTime = timeoutTime;
scheduledHostCallback = null;
timeoutTime = -1;

var currentTime = exports.unstable_now();

var didTimeout = false;
// 说明没有时间了 当前帧给与这个 callback 的时间没有了
if (frameDeadline – currentTime <= 0) {
// There’s no time left in this idle period. Check if the callback has
// a timeout and whether it’s been exceeded.
// 检查当前 callback 是否过期 和 是否被执行
// previousFrameTime 小于等于 currentTime 时,scheduler
// 认为线程不是空闲的,对于超时的任务将立即执行,
// 对于未超时的任务将在下次重绘后予以处理
// 显然是超时的 并且没有被取消 直接执行 并且给与 timeout 为空
if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there’s no
// time left.
didTimeout = true;
} else {
// No timeout.
// 没有超时 如果没有安排轮询 就开启轮询
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrameWithTimeout(animationTick);
}
// Exit without invoking the callback.
// 不执行 callback 直接退出 让轮询去调度
scheduledHostCallback = prevScheduledCallback;
timeoutTime = prevTimeoutTime;
return;
}
}

// 执行 callback 这里应该是终点了 到这里为止 调度分析完了
// 执行完成的调度 返回的只有 是否已经过期 (didTimeout)
if (prevScheduledCallback !== null) {
isFlushingHostCallback = true;
try {
prevScheduledCallback(didTimeout);
} finally {
isFlushingHostCallback = false;
}
}
};

// 这里作为 rAF 的 callback 处理函数
/**
* 在 animateTick 中,scheduler 将计算下一帧期望完成时间点 previousFrameTime,
然后通过 port.postMessage 方法发送消息。等到 port1 接受到消息时,schdulear
将 previousFrameTime 与 currentTime 作比较:当 previousFrameTime 小于等于 currentTime 时,
scheduler 认为线程不是空闲的,对于超时的任务将立即执行,对于未超时的任务将在下次重绘后予以处理;
当 previousFrameTime 大于 currentTime 时,线程就是空闲的,scheduler 将立即执行。这一处理机制在
port1.onMessage 监听函数中实现(作为 macrotasks,port1 接受消息的时机将随着线程的空闲程度起变化)。

*/
var animationTick = function (rafTime) {
// 轮询了 这里进入
if (scheduledHostCallback !== null) {
// Eagerly schedule the next animation callback at the beginning of the
// frame. If the scheduler queue is not empty at the end of the frame, it
// will continue flushing inside that callback. If the queue *is* empty,
// then it will exit immediately. Posting the callback at the start of the
// frame ensures it’s fired within the earliest possible frame. If we
// waited until the end of the frame to post the callback, we risk the
// browser skipping a frame and not firing the callback until the frame
// after that.
/**
*
* 最先在帧的开头安排下一个回调。如果调度程序队列在帧的末尾不为空,
* 它将继续在该回调内刷新。如果队列 * 为 * 空,则它将立即退出
*。在帧的开头触发回调可确保在最早的帧内触发。要是我们
* 等到帧结束后触发回调,我们冒着浏览器丢帧的风险,
* 并且在此帧之后的不会触发回调。
*/
requestAnimationFrameWithTimeout(animationTick);
} else {
// No pending work. Exit.
isAnimationFrameScheduled = false;
return;
}

// 调度的时间 rafTime – frameDeadline 下一帧预到期 + 一帧的多少 = 给下一帧留下的时间
var nextFrameTime = rafTime – frameDeadline + activeFrameTime;

// 帧的频率小于当前的 说明处理的时间都是比较短的
// 其实这里做了下调整 如果当前的设备的更新频率大于我们设定的 30fps
// 我们就需要取更新的频率的最大值 这里的最大值的更新频率 最大值
// 我们需要澄清一个问题 频率越大 一帧花费的时间就越短
if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime) {
if (nextFrameTime < 8) {
// 预防性代码 也就是说 不支持刷新频率大于 120hz 如果大于 120 就当 120 处理 也就是说 一帧只有 8ms
// Defensive coding. We don’t support higher frame rates than 120hz.
// If the calculated frame time gets lower than 8, it is probably a bug.
nextFrameTime = 8;
}
// If one frame goes long, then the next one can be short to catch up.
// If two frames are short in a row, then that’s an indication that we
// actually have a higher frame rate than what we’re currently optimizing.
// We adjust our heuristic dynamically accordingly. For example, if we’re
// running on 120hz display or 90hz VR display.
// Take the max of the two in case one of them was an anomaly due to
// missed frame deadlines.

// 如果一帧长,那么下一帧可能很短。
// 如果两个帧连续短,那么这表明我们实际上具有比我们当前优化
// 的帧速率更高的帧速率。我们相应地动态调整启发式。例如,如果我们是
// 在 120hz 显示屏或 90hz VR 显示屏上运行。
// 取两个中的最大值,以防其中一个因错过帧截止日期而异常。
activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime;

} else {
previousFrameTime = nextFrameTime;
}

// 计算下一帧过期时间
frameDeadline = rafTime + activeFrameTime;
// 如果还么发送消息 就触发 message 让帧重绘完成后 进行调度 callback
if (!isMessageEventScheduled) {
isMessageEventScheduled = true;
port.postMessage(undefined);
}
};

//
requestHostCallback = function (callback, absoluteTimeout) {
// 设置当前的 callback
scheduledHostCallback = callback;
// 设置过期时间
timeoutTime = absoluteTimeout;
// 当前如果有任务正在执行中(意为当前没有重绘任务,重绘线程是空闲的)
// 或者所添加的任务需要立即执行,scheduler 直接调用 port.postMessage 发送消息,跳过 rAF
// 轮询,以使任务得到即时执行
if (isFlushingHostCallback || absoluteTimeout < 0) {
// Don’t wait for the next frame. Continue working ASAP, in a new event.
port.postMessage(undefined);
} else if (!isAnimationFrameScheduled) {
// If rAF didn’t already schedule one, we need to schedule a frame.
// 如果 raf 没有进行调度 安排一个新的 rAF 轮询
// 如果 rAF 没有发挥作用 在使用 settimeout 去作为预备去调度
// TODO: If this rAF doesn’t materialize because the browser throttles, we
// might want to still have setTimeout trigger rIC as a backup to ensure
// that we keep performing work.
isAnimationFrameScheduled = true;

// 如果 rAF 轮询未启动,调用 requestAnimationFrameWithTimeout(animationTick) 启动轮询
requestAnimationFrameWithTimeout(animationTick);
}
};

cancelHostCallback = function () {
scheduledHostCallback = null;
isMessageEventScheduled = false;
timeoutTime = -1;
};
}
7 执行到调度程序 callback 这里 callback 是怎么执行的(flushWork)
flushFirstCallback
flushFirstCallback 从双向链表中取出首个任务节点并执行。若首个任务节点的 callback 返回函数,使用该函数构建新的 callbackNode 任务节点,并将该任务节点插入双向链表中:若该任务节点的优先级最高、且不只包含一个任务节点,调用 ensureHostCallbackIsScheduled,在下一次重绘后酌情执行双向链表中的任务节点;否则只将新创建的任务节点添加到双向链表中

function flushFirstCallback() {
var flushedNode = firstCallbackNode;

// Remove the node from the list before calling the callback. That way the
// list is in a consistent state even if the callback throws.
var next = firstCallbackNode.next;
if (firstCallbackNode === next) {
// This is the last callback in the list.
firstCallbackNode = null;
next = null;
} else {
var lastCallbackNode = firstCallbackNode.previous;
firstCallbackNode = lastCallbackNode.next = next;
next.previous = lastCallbackNode;
}

flushedNode.next = flushedNode.previous = null;

// Now it’s safe to call the callback.
var callback = flushedNode.callback;
var expirationTime = flushedNode.expirationTime;
var priorityLevel = flushedNode.priorityLevel;
var previousPriorityLevel = currentPriorityLevel;
var previousExpirationTime = currentExpirationTime;
currentPriorityLevel = priorityLevel;
currentExpirationTime = expirationTime;
var continuationCallback;
try {
continuationCallback = callback();
} finally {
// 恢复当一次的优先级
currentPriorityLevel = previousPriorityLevel;
currentExpirationTime = previousExpirationTime;
}

// A callback may return a continuation. The continuation should be scheduled
// with the same priority and expiration as the just-finished callback
//. 如果 callback 返回的还是 function 需要重新调度
// 跟新加入一个节点是一样的 就不在分析了
if (typeof continuationCallback === ‘function’) {
var continuationNode = {
callback: continuationCallback,
priorityLevel: priorityLevel,
expirationTime: expirationTime,
next: null,
previous: null
};

// Insert the new callback into the list, sorted by its expiration. This is
// almost the same as the code in `scheduleCallback`, except the callback
// is inserted into the list *before* callbacks of equal expiration instead
// of after.
if (firstCallbackNode === null) {
// This is the first callback in the list.
firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode;
} else {
var nextAfterContinuation = null;
var node = firstCallbackNode;
do {
if (node.expirationTime >= expirationTime) {
// This callback expires at or after the continuation. We will insert
// the continuation *before* this callback.
nextAfterContinuation = node;
break;
}
node = node.next;
} while (node !== firstCallbackNode);

if (nextAfterContinuation === null) {
// No equal or lower priority callback was found, which means the new
// callback is the lowest priority callback in the list.
nextAfterContinuation = firstCallbackNode;
} else if (nextAfterContinuation === firstCallbackNode) {
// The new callback is the highest priority callback in the list.
firstCallbackNode = continuationNode;
ensureHostCallbackIsScheduled();
}

var previous = nextAfterContinuation.previous;
previous.next = nextAfterContinuation.previous = continuationNode;
continuationNode.next = nextAfterContinuation;
continuationNode.previous = previous;
}
}
}
flushImmediateWork
基于 flushFirstCallback,flushImmediateWork 函数用于执行双向链表中所有优先级为 ImmediatePriority 的任务节点。如果双向链表不只包含优先级为 ImmediatePriority 的任务节点,flushImmediateWork 将调用 ensureHostCallbackIsScheduled 等待下次重绘后执行剩余的任务节点。
function flushImmediateWork() {
if (
// Confirm we’ve exited the outer most event handler
// 确认我们退出了最外层的事件 handler
// 执行所有立即执行的 callback
currentEventStartTime === -1 && firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority) {
isExecutingCallback = true;
try {
do {
flushFirstCallback();
} while (
// Keep flushing until there are no more immediate callbacks
firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority);
} finally {
isExecutingCallback = false;

// 还有其他优先级的 依次轮询调度
if (firstCallbackNode !== null) {
// There’s still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
}
}
}
flushWork
flushWork 作为 requestHostCallback 函数的参数,获得的首个实参 didTimeout 为是否超时的标识。如果超时,flushWork 通过调用 flushFirstCallback 批量执行所有未超时的任务节点;若果没有超时,flushWork 将在下一帧未完成前(通过 shouldYieldToHost 函数判断)尽可能地执行任务节点。等上述条件逻辑执行完成后,如果双向链表非空,调用 ensureHostCallbackIsScheduled 等待下次重绘后执行剩余的任务节点。特别的,当双向链表中还存在 ImmediatePriority 优先级的任务节点,flushWork 将调用 flushImmediateWork 批量执行这些任务节点。
function flushWork(didTimeout) {
// Exit right away if we’re currently paused
// 暂停情况下 直接退出
if (enableSchedulerDebugging && isSchedulerPaused) {
return;
}

isExecutingCallback = true;
var previousDidTimeout = currentDidTimeout;
currentDidTimeout = didTimeout;
try {
// 如果已经超时
if (didTimeout) {
// Flush all the expired callbacks without yielding.
// 让 firstCallbackNode 双向链表去消耗
while (firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused)) {
// TODO Wrap in feature flag
// Read the current time. Flush all the callbacks that expire at or
// earlier than that time. Then read the current time again and repeat.
// This optimizes for as few performance.now calls as possible.
var currentTime = exports.unstable_now();

// 已经过期的 直接执行
if (firstCallbackNode.expirationTime <= currentTime) {
do {
flushFirstCallback();
} while (firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused));
continue;
}
break;
}
} else {
// Keep flushing callbacks until we run out of time in the frame.
if (firstCallbackNode !== null) {
do {
if (enableSchedulerDebugging && isSchedulerPaused) {
break;
}
flushFirstCallback();
// 没有超时 也不用放入下一帧的的直接执行
} while (firstCallbackNode !== null && !shouldYieldToHost());
}
}
} finally {
isExecutingCallback = false;
currentDidTimeout = previousDidTimeout;
// 没有处理玩的继续的执行
if (firstCallbackNode !== null) {
// There’s still work remaining. Request another callback.
ensureHostCallbackIsScheduled();
} else {
isHostCallbackScheduled = false;
}
// Before exiting, flush all the immediate work that was scheduled.
// 退出之前将所有立即执行的任务去执行
flushImmediateWork();
}
}

因为 scheduler 使用首个任务节点的超时时间点作为 requestHostCallback 函数的次参(在 ensureHostCallbackIsScheduled 函数中处理)。因此,如果首个任务节点的优先级为 ImmediatePriority,flushWork 所获得参数 didTimeout 也将是否值,其执行逻辑将是执行所有优先级为 ImmediatePriority 的任务节点,再调用 ensureHostCallbackIsScheduled 等待下一次重绘时执行其余任务节点。如果首个任务节点的优先级为 UserBlockingPriority 等,flushWork 将执行同优先级的任务节点,再调用 ensureHostCallbackIsScheduled 等待下一次重绘时执行其余任务节点。所有对不同优先级的任务节点,scheduler 采用分段执行的策略
8、其他 API
unstable_runWithPriority
function unstable_runWithPriority(priorityLevel, eventHandler) {
switch (priorityLevel) {
case ImmediatePriority:
case UserBlockingPriority:
case NormalPriority:
case LowPriority:
case IdlePriority:
break;
default:
priorityLevel = NormalPriority;
}

var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = priorityLevel;
currentEventStartTime = getCurrentTime();

try {
return eventHandler();
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;

// Before exiting, flush all the immediate work that was scheduled.
flushImmediateWork();
}
}
unstable_runWithPriority(priorityLevel, eventHandler) 将 currentPriorityLevel 缓存设置为 priorityLevel,随后再执行 eventHandler,最后调用 flushImmediateWork 函数执行所有优先级为 ImmediatePriority 的任务节点,其余任务节点等待下次重绘后再执行。可以设想,当 eventHandler 为 unstable_scheduleCallback 函数时,将影响所添加任务节点的优先级,并立即执行 ImmediatePriority 优先级的任务。其实就是给执行 eventHandler 设置优先级
unstable_wrapCallback
function unstable_wrapCallback(callback) {
var parentPriorityLevel = currentPriorityLevel;
return function () {
// This is a fork of runWithPriority, inlined for performance.
var previousPriorityLevel = currentPriorityLevel;
var previousEventStartTime = currentEventStartTime;
currentPriorityLevel = parentPriorityLevel;
currentEventStartTime = exports.unstable_now();

try {
return callback.apply(this, arguments);
} finally {
currentPriorityLevel = previousPriorityLevel;
currentEventStartTime = previousEventStartTime;
flushImmediateWork();
}
};
}
unstable_wrapCallback(callback) 记录当前的优先级 currentPriorityLevel,返回函数处理效果如 unstable_runWithPriority,对于 callback 中新添加的任务节点将使用所记录的 currentPriorityLevel 作为优先级。这里可以返回的是 function 将作为新的节点去插入被调度
9 其他

unstable_pauseExecution 通过将 isSchedulerPaused 置为 true,打断 scheduler 处理任务节点。
unstable_continueExecution 取消打断状态,使 scheduler 恢复处理任务节点。
unstable_getFirstCallbackNode 获取双向链表中的首个任务节点。
unstable_cancelCallback(callbackNode) 从双向链表中移除指定任务节点。
unstable_getCurrentPriorityLevel 获取当前优先级 currentPriorityLevel 缓存。
unstable_shouldYield 是否需要被打断。
unstable_now 获取当前时间。

10 总结
读完 scheduler 源码 感觉还是挺复杂的 当然收获也是比较大的 尤其是对于浏览执行机制有了更深入的认识 尤其调度思路让人影响时刻,当然分析肯定会有不全面或者偏差的地方 欢迎大佬们指正

退出移动版