scheduler模块react为了实现fiber架构新增的一个模块,主要用来实现任务的拆分和调度执行。
scheduler是一个独立的模块,暴露了一些任务调度相关的方法,可以直接调用。
我们可以从npm仓库直接安装使用:
const scheduler = require('scheduler')
scheduler.unstable_scheduleCallback(scheduler.unstable_ImmediatePriority, () => {
console.log(111)
})
scheduler.unstable_scheduleCallback(scheduler.unstable_NormalPriority, () => {
console.log(222)
})
scheduleCallback可以认为是 scheduler 的入口方法,用于添加任务。这个方法主要有3个参数
- priorityLevel:任务的优先级
- callback:回调,任务的具体逻辑
- options:额外的参数,可以设置delay
scheduler 根据重要程度给任务分成了5个优先级
- NoPriority = 0;
- ImmediatePriority = 1;
- UserBlockingPriority = 2;
- NormalPriority = 3;
- LowPriority = 4;
- IdlePriority = 5;
除了 NoPriority表示最低的优先级外,剩余的5个数值越小优先级越高。
下面看一下scheduleCallback方法
function unstable_scheduleCallback(priorityLevel, callback, options) {
var currentTime = getCurrentTime(); // 通过performance.now()获取当前时间
// 判断 options 参数中是否有延迟字段( delay ),有的话把 currentTime 和 delay 相加
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;
}
// 这里需要说一下延迟时间(delay)和超时时间(timeout)的区别,延迟时间是业务定义的必须执行等待的;
// 而超时时间是根据任务优先级映射的时间,主要作用是给任务按优先级排序执行,只要没有高优先级的任务了,低优先级的任务也会立即执行。
// 当前时间(currentTime) + 延迟时间(delay) + 超时时间(timeout) = 任务的过期时间
var expirationTime = startTime + timeout;
// 新建一个任务
var newTask = {
id: taskIdCounter++, // 递增的id
callback, // 业务逻辑
priorityLevel, // 优先级
startTime, // 任务开始执行时间 currentTime + delay
expirationTime, // 过期时间 currentTime + delay + timeout
sortIndex: -1, // 排序索引
};
// 所有profile相关的都是用于代码调试,可忽略
if (enableProfiling) {
newTask.isQueued = false;
}
if (startTime > currentTime) { // 有延迟(delay)的情况
// This is a delayed task.
newTask.sortIndex = startTime; // 设置排序索引为开始时间,通过开始时间来给任务的执行排序
push(timerQueue, newTask); // 把任务放入到timerQueue,timerQueue是用来存储有延迟(delay)的任务,当延迟时间到了时会把任务从timerQueue转移到taskQueue中
if (peek(taskQueue) === null && newTask === peek(timerQueue)) { // 如果没有可执行的任务,并且上面新创建的任务是下一次待执行的任务
// 如果最近的一个任务需要延迟执行,则通过setTimeout方法,暂停delay时间后执行任务。主要用于性能优化,在没有任务时暂停代码运行
// 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 { // 没有延迟(delay)的情况,说明当前肯定有任务需要执行
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); // 在下一个 event loop 执行 flushWork 方法
}
}
return newTask;
}
下面我们看一下 requestHostCallback 方法
function requestHostCallback(callback) {
scheduledHostCallback = callback; // 把 flushWork 方法赋给 scheduledHostCallback
if (!isMessageLoopRunning) { // 是否有任务正在执行
isMessageLoopRunning = true;
port.postMessage(null); // Scheduler 首选用 MessageChannel 调度下一个 event loop,在不支持 MessageChannel 的环境,或 nodejs 环境也 polyfill 了 setTimeout 的实现版本。主要的原因是 setTimout 方法自带了几毫秒的延时,在有大量的任务时会有比较大的性能问题
}
}
具体的 MessageChannel 的应用
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline; // 当收到 postMessage 方法时会调用 performWorkUntilDeadline
每次 event loop 在 performWorkUntilDeadline 方法中处理
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) { // scheduledHostCallback 表示 flushWork
const currentTime = getCurrentTime(); // 获取当前时间
// Yield after `yieldInterval` ms, regardless of where we are in the vsync
// cycle. This means there's always time remaining at the beginning of
// the message event.
deadline = currentTime + yieldInterval; // yieldInterval 表示每个时间片可以执行任务的时间长度,默认5毫秒,可以通过 forceFrameRate 方法动态计算;在每个时间片内还会有多次 postMessage
const hasTimeRemaining = true; // 是否还有剩余时间
try {
const hasMoreWork = scheduledHostCallback(hasTimeRemaining, currentTime); // 执行 flushWork 方法
if (!hasMoreWork) { // 如果没有可执行的任务了
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// If there's more work, schedule the next message event at the end
// of the preceding one.
port.postMessage(null); // 如果还有可执行的任务 postMessage 递归调用 performWorkUntilDeadline 和 flushWork 方法
}
} catch (error) {
// If a scheduler task throws, exit the current browser task so the
// error can be observed.
port.postMessage(null);
throw error;
}
} else {
isMessageLoopRunning = false;
}
// Yielding to the browser will give it a chance to paint, so we can
// reset this.
needsPaint = false;
};
下面看一下 flushWork 方法
function flushWork(hasTimeRemaining, initialTime) {
if (enableProfiling) {
markSchedulerUnsuspended(initialTime);
}
// We'll need a host callback the next time work is scheduled.
isHostCallbackScheduled = false; // 取消暂停调度
if (isHostTimeoutScheduled) { // 把延迟暂停取消
// We scheduled a timeout but it's no longer needed. Cancel it.
isHostTimeoutScheduled = false;
cancelHostTimeout();
}
isPerformingWork = true; // 标识正在执行任务
const previousPriorityLevel = currentPriorityLevel; // 缓存当前的优先级
try {
if (enableProfiling) {
try {
return workLoop(hasTimeRemaining, initialTime);
} catch (error) {
if (currentTask !== null) {
const currentTime = getCurrentTime();
markTaskErrored(currentTask, currentTime);
currentTask.isQueued = false;
}
throw error;
}
} else {
// No catch in prod code path.
return workLoop(hasTimeRemaining, initialTime); // 具体执行任务
}
} finally {
// 执行完成所有taskQueue中的任务
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false; // 标识执行任务结束
if (enableProfiling) {
const currentTime = getCurrentTime();
markSchedulerSuspended(currentTime);
}
}
}
看 workLoop 方法
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime; // 执行任务开始时间
advanceTimers(currentTime); // 把不再有延迟的任务(delay时间到了)从 timerQueue 放入到 taskQueue
currentTask = peek(taskQueue); // 从 taskQueue 按排序取下一个需要执行的任务
while (
currentTask !== null && // 任务不为空
!(enableSchedulerDebugging && isSchedulerPaused) // 这2个参数用于debug
) {
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; // 任务过期了
markTaskRun(currentTask, currentTime); // 用于调试
const continuationCallback = callback(didUserCallbackTimeout); // 执行任务, 任务有一个参数,表示任务是否过期
currentTime = getCurrentTime(); // 获取当前时间
if (typeof continuationCallback === 'function') { // 如果任务会返回一个函数
currentTask.callback = continuationCallback; // 把返回的函数赋值给当前任务的回调
markTaskYield(currentTask, currentTime); // 用于调试
} else { // 否则,删除该次任务
if (enableProfiling) {
markTaskCompleted(currentTask, currentTime);
currentTask.isQueued = false;
}
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
advanceTimers(currentTime); // 再次执行: 把不再有延迟的任务(delay时间到了)从 timerQueue 放入到 taskQueue
} else {
pop(taskQueue);
}
currentTask = peek(taskQueue); // 取出下一个任务
}
// Return whether there's additional work
if (currentTask !== null) { // 如果存在下一个任务,结束调用,进入下一次 event loop
return true;
} else {
// 如果不存在下一个任务,并且存在延迟的任务,暂停delay时间后再执行任务
const firstTimer = peek(timerQueue);
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
return false;
}
}
最后一个没有仔细讲到的点是任务的排序,我们可以看一下 在 SchedulerMinHeap 文件中定义的几个方法
// 把任务放入到队列中,并执行 siftUp 方法
export function push(heap: Heap, node: Node): void {
const index = heap.length;
heap.push(node);
siftUp(heap, node, index);
}
// 取出第一个任务
export function peek(heap: Heap): Node | null {
const first = heap[0];
return first === undefined ? null : first;
}
// 返回第一个任务,把最后一个任务放到第一个位置,调用siftDown
export function pop(heap: Heap): Node | null {
const first = heap[0];
if (first !== undefined) {
const last = heap.pop();
if (last !== first) {
heap[0] = last;
siftDown(heap, last, 0);
}
return first;
} else {
return null;
}
}
// siftUp 方法,push 后调用
function siftUp(heap, node, i) {
let index = i; // i 是 push 后的任务的位置,也就是列表的最后一个位置
while (true) {
const parentIndex = (index - 1) >>> 1;// 把位置做位移操作,比如1 >>> 1 = 0, 2 >>> 1 = 1, , 4 >>> 1 = 2
const parent = heap[parentIndex]; // 获取位移后的位置的任务
if (parent !== undefined && compare(parent, node) > 0) { // 根据任务的sortIndex比较任务的大小,如果sortIndex相同,就根据任务的id比较;sortIndex就是任务的开始时间(currentTime + delay)。
// The parent is larger. Swap positions.
// 如果parent任务的开始时间比较晚,则和当前任务进行交互
// 其实就是个排序算法,按任务开始时间or任务过期时间进行排序
heap[parentIndex] = node;
heap[index] = parent;
index = parentIndex;
} else {
// The parent is smaller. Exit.
return;
}
}
}
// siftDown 方法,pop 后调用
// 这个方法一开始很不理解,为什么要把最后一个任务放到第一个任务位置上,再给它排序
// 后来想了想,个人感觉是为了避免优先级较低的任务长时间不执行
function siftDown(heap, node, i) {
let index = i;
const length = heap.length;
while (index < length) {
const leftIndex = (index + 1) * 2 - 1;
const left = heap[leftIndex];
const rightIndex = leftIndex + 1;
const right = heap[rightIndex];
// If the left or right node is smaller, swap with the smaller of those.
if (left !== undefined && compare(left, node) < 0) {
if (right !== undefined && compare(right, left) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
heap[index] = left;
heap[leftIndex] = node;
index = leftIndex;
}
} else if (right !== undefined && compare(right, node) < 0) {
heap[index] = right;
heap[rightIndex] = node;
index = rightIndex;
} else {
// Neither child is smaller. Exit.
return;
}
}
}
function compare(a, b) {
// Compare sort index first, then task id.
const diff = a.sortIndex - b.sortIndex;
return diff !== 0 ? diff : a.id - b.id;
}