一、任务调度的入口
无论是首次渲染,还是后续更新操作,都会进入到react-reconciler对外暴露的updateContainer函数,然后在该函数中调用scheduleUpdateOnFiber,因此scheduleUpdateOnFiber函数是任务调度的入口。
函数的调用顺序:
updateContainer -> scheduleUpdateOnFiber(任务输入入口)-> ensureRootIsScheduled(安排调度任务)-> unstable_scheduleCallback(创建调度任务)-> 调用requestHostCallback
二、ensureRootIsScheduled(安排调度任务)
如果任务类型是并发任务,则需要经过调度,会通过scheduleCallback回调函数注册调度任务。
// 使用这个函数为 root 安排任务,每个 root 只有一个任务
// 每次更新时都会调用此函数,并在退出任务之前调用此函数。
function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
// Schedule a new callback.
// 重新安排一个新的渲染任务
let newCallbackNode;
// 如果新渲染任务的优先级是同步优先级 if 逻辑处理的是同步任务,同步任务不需经过 Scheduler
if (newCallbackPriority === SyncLane) {
// 同步任务不经过 Scheduler,任务执行入口是 performSyncWorkOnRoot 函数
if (root.tag === LegacyRoot) {
// LegacyRoot 启动模式 把这个渲染任务加入 syncQueue 队列中
scheduleLegacySyncCallback(performSyncWorkOnRoot.bind(null, root));
} else {
// 启动模式为非 legacy模式 把这个渲染任务加入 syncQueue 队列中
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
}
} else {
// else 逻辑处理的是并发任务,并发任务需要经过 Scheduler 根据调度优先级,调度并发任务
newCallbackNode = scheduleCallback(
schedulerPriorityLevel,
performConcurrentWorkOnRoot.bind(null, root),
);
}
}
三、unstable_scheduleCallback(创建调度任务)
scheduleCallback 函数是引用了 packages/scheduler/src/Scheduler.js 路径下的 unstable_scheduleCallback 函数,通过unstable_scheduleCallback函数创建调度任务(newTask),newTask的callback实际上就是performConcurrentWorkOnRoot。然后根据任务是否超时,分别将任务插入到超时队列timerQueue 和调度任务队列taskQueue中。将任务插入调度任务队列taskQueue之后,会通过requestHostCallback函数去调度任务。
// 省略部分无关代码
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 1. 获取当前时间
var currentTime = getCurrentTime();
var startTime;
if (typeof options === 'object' && options !== null) {
// 从函数调用关系来看, 在v17.0.2中,所有调用 unstable_scheduleCallback 都未传入options
// 所以省略延时任务相关的代码
} else {
startTime = currentTime;
}
// 2. 根据传入的优先级, 设置任务的过期时间 expirationTime
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;
// 3. 创建新任务
var newTask = {
id: taskIdCounter++,
callback,
priorityLevel,
startTime,
expirationTime,
sortIndex: -1,
};
if (startTime > currentTime) {
// 省略无关代码 v17.0.2中不会使用
} else {
newTask.sortIndex = expirationTime;
// 4. 加入任务队列
push(taskQueue, newTask);
// 5. 请求调度
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork);
}
}
return newTask;
}
四、调用requestHostCallback进行调度
4.1 requestHostCallback的调用
创建任务之后, 最后请求调度requestHostCallback(flushWork), flushWork函数作为参数被传入调度中心内核等待回调.。requestHostCallback函数在上文调度内核中已经介绍过了, 在调度中心中, 只需下一个事件循环就会执行回调, 最终执行flushWork。
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
// 请求回调
requestHostCallback = function(callback) {
// 1. 保存callback
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
// 2. 通过 MessageChannel 发送消息
port.postMessage(null);
}
};
4.2 flushWork的调用
flushWork中调用了workLoop. 队列消费的主要逻辑是在workLoop函数中, 这就是React 工作循环一文中提到的任务调度循环.
// 省略无关代码
function flushWork(hasTimeRemaining, initialTime) {
// 1. 做好全局标记, 表示现在已经进入调度阶段
isHostCallbackScheduled = false;
isPerformingWork = true;
const previousPriorityLevel = currentPriorityLevel;
try {
// 2. 循环消费队列
return workLoop(hasTimeRemaining, initialTime);
} finally {
// 3. 还原全局标记
currentTask = null;
currentPriorityLevel = previousPriorityLevel;
isPerformingWork = false;
}
}
4.3 workloop的调用
workloop函数实际上是一个大循环,会不断的去队列中获取到任务。任务执行之后会有回调,回调的结果是函数的话就会继续执行,如果不是函数的话就会将当前的任务移除队列。然后currentTask重新赋值,新的值是从队列取出的新的任务,知道 currentTask 的值为null才打断循环。最后会return true或者false来通知任务是否执行结束。
// 省略部分无关代码
function workLoop(hasTimeRemaining, initialTime) {
let currentTime = initialTime; // 保存当前时间, 用于判断任务是否过期
currentTask = peek(taskQueue); // 获取队列中的第一个任务
while (currentTask !== null) {
if (currentTask.expirationTime > currentTime &&(!hasTimeRemaining || shouldYieldToHost())) {
// 虽然currentTask没有过期, 但是执行时间超过了限制(毕竟只有5ms, shouldYieldToHost()返回true). 停止继续执行, 让出主线程
break;
}
const callback = currentTask.callback;
if (typeof callback === 'function') {
currentTask.callback = null;
currentPriorityLevel = currentTask.priorityLevel;
const didUserCallbackTimeout = currentTask.expirationTime <= currentTime;
// 此时,执行 callback,即 performConcurrentWorkOnRoot 方法
// 在执行 performConcurrentWorkOnRoot 方法的过程中,如果 reconciler 中的 workLoop 中断了
// 会返回 performConcurrentWorkOnRoot 自身方法,也就是 continuationCallback 会被放到当前 task 的 callback
// 此时 workLoop 的 while 循环中断,但是由于当前 task 并没有从队列中出来,
// 所以下一次执行 workLoop 时,仍然会执行本次存储的 continuationCallback
const continuationCallback = callback(didUserCallbackTimeout);
currentTime = getCurrentTime();
// 回调完成, 判断是否还有连续(派生)回调
if (typeof continuationCallback === 'function') {
// 产生了连续回调(如fiber树太大, 出现了中断渲染), 保留currentTask
currentTask.callback = continuationCallback;
} else {
// 把currentTask移出队列
if (currentTask === peek(taskQueue)) {
pop(taskQueue);
}
}
} else {
// 如果任务被取消(这时currentTask.callback = null), 将其移出队列
pop(taskQueue);
}
// 更新currentTask
currentTask = peek(taskQueue);
}
if (currentTask !== null) {
return true; // 如果task队列没有清空, 返回true. 等待调度中心下一次回调
} else {
return false; // task队列已经清空, 返回false.
}
}
shouldYieldToHost 函数详解:是否让出主线程,注意shouldYieldToHost的判定条件:
-
currentTime >= deadline: 只有时间超过deadline之后才会让出主线程(其中deadline = currentTime + yieldInterval).
-
yieldInterval默认是5ms, 只能通过forceFrameRate函数来修改(事实上在 v17.0.2 源码中, 并没有使用到该函数).
-
如果一个task运行时间超过5ms, 下一个task执行之前, 会把控制权归还浏览器.
-
navigator.scheduling.isInputPending(): 用于判断是否有输入事件(包括: input 框输入事件, 点击事件等)
// 是否让出主线程 shouldYieldToHost = function() { const currentTime = getCurrentTime(); if (currentTime >= deadline) { if (needsPaint || scheduling.isInputPending()) { return true; } return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms, 这个判断只在初始化过程中才有可能返回false } else {. return false; }