概览
React应用中的更新(如useEffect、setState等)都会被转化为一个任务task,并将其分配给Scheduler进行调度。每个任务都有一个优先级priority,而Scheduler会根据优先级来决定任务的执行顺序。
优先级
优先级定义
js
// React 中的优先级定义(数值越小,优先级越高)
const NoPriority = 0; // 无优先级
const ImmediatePriority = 1; // 立即执行优先级
const UserBlockingPriority = 2; // 用户阻塞优先级
const NormalPriority = 3; // 正常优先级
const LowPriority = 4; // 低优先级
const IdlePriority = 5; // 空闲优先级
优先级具体含义
ImmediatePriority
- 用途:需要立即同步执行的任务
- 场景:
- 离散的用户输入(如点击、按键)
- 同步的React渲染
- 在并发模式
Concurrent Mode中,某些需要立即执行的副作用
- 特点:不能被中断,必须立即完成
UserBlockingPriority
- 用途:用户交互相关的任务
- 场景:
- 连续的用户输入(如拖拽、滚动)
- 动画更新
- 用户触发的状态更新
- 特点:需要在下一帧前完成,确保流畅的用户体验
NormalPriority
- 用途:默认的任务优先级
- 场景:
- 大部分状态更新
- 网络请求完成后的渲染
- 普通的副作用
- 特点:可以被更高优先级的任务打断
LowPriority
- 用途:可以延迟执行的任务
- 场景:
- 数据预加载
- 非关键的渲染
- 分析日志
- 特点:在浏览器空闲时执行
IdlePriority
- 用途:在浏览器完全空闲时执行的任务
- 场景:
- 在非必要的后台任务
- 性能监控
- 离线数据同步
- 特点:只在浏览器空闲时执行,可能永远不会执行。
优先级作用
Scheduler调度器通过两个队列来管理调度任务:taskQueue任务队列和timerQueue定时器队列,以下是它们的区别:
taskQueue任务队列
- 存储可立即执行的任务
- 按
expirationTime排序 - 存储当前需要执行的任务
timerQueue定时器队列
- 存储延迟执行的任务
- 按
startTime排序 - 用于管理未来要执行的任务
这两个队列都是按照最小堆排序,而优先级就是排序的关键,优先级最小的在队列的最前面。
任务的注册unstable_scheduleCallback
unstable_scheduleCallback是Scheduler调度器提供给Reconciler协调器的方法,用于创建任务,并将其放入队列中进行调度执行。其源码实现如下:
js
function unstable_scheduleCallback(priorityLevel, callback, options) {
// 获取当前时间
var currentTime = getCurrentTime();
var startTime;
// 判断参数options是否有值,其delay值为延迟执行的时间,若设置了delay,则startTime为currentTime和delay之和;反之,startTime就是currentTime,表示需要立即执行
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 = -1;
break;
case UserBlockingPriority:
timeout = 250;
break;
case IdlePriority:
timeout = Math.pow(2, 30) - 1;
break;
case LowPriority:
timeout = 10000;
break;
case NormalPriority:
default:
timeout = 5000;
break;
}
// 计算过期时间
var expirationTime = startTime + timeout;
// 创建任务对象
var newTask = {
id: taskIdCounter++, // 唯一标识
callback, // 实际要执行的任务
priorityLevel, // 原始优先级
startTime, // 何时可以开始执行
expirationTime, // 何时必须执行(过期必须执行)
sortIndex: -1, // 在队列中的排序依据,初始值为-1
};
// 延时任务
if (startTime > currentTime) {
// 延时任务按照开始时间排序
newTask.sortIndex = startTime;
// 将任务插入到timerQueue队列中,push方法会使得timerQueue队列重排序,sortIndex最小的任务优先级最高,会在队列最前面
push(timerQueue, newTask);
// 若任务队列为空,且新任务是定时器队列最早的任务,则满足条件
if (peek(taskQueue) === null && newTask === peek(timerQueue)) {
// 若 设置过定时器,则取消定时器
if (isHostTimeoutScheduled) {
cancelHostTimeout();
} else {
// 修改定时器标志
isHostTimeoutScheduled = true;
}
// 调用requestTimeout方法,创建定时器
requestHostTimeout(handleTimeout, startTime - currentTime);
}
} else {
// 若没有设置延迟时间delay,则将任务的过期时间设为任务的sortIndex
newTask.sortIndex = expirationTime;
// 将新任务放到taskQueue任务队列中
push(taskQueue, newTask);
// 若没有正在进行的调度且没有正在执行的工作,则调用requestHostCallback
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback();
}
}
// 返回任务对象
return newTask;
}
延时任务队列
requestHostTimeout
requestHostTimeout方法会创建一个定时器,在ms后,调用callback方法。在注册延时任务时,满足条件后,会在delay毫秒后调用的callback回调就是handleTimeout方法。
js
function requestHostTimeout(callback, ms) {
taskTimeoutID = localSetTimeout(() => {
callback(getCurrentTime());
}, ms);
}
handleTimeout
handleTimeout方法在延迟时间到了后就会触发。
js
function handleTimeout(currentTime) {
// 将定时器标志置为false,方便后续创建定时器
isHostTimeoutScheduled = false;
// 处理定时器队列
advanceTimers(currentTime);
// 若此时没有任务执行
if (!isHostCallbackScheduled) {
// 任务队列不为空
if (peek(taskQueue) !== null) {
// 修改任务执行标志
isHostCallbackScheduled = true;
// 调用requestHostCallback调度任务
requestHostCallback();
} else {
// 若任务队列为空,则从定时器队列中取出优先级最高的定时器任务
const firstTimer = peek(timerQueue);
// 若定时器任务存在,则调用requestHostTimeout创建新的定时器
if (firstTimer !== null) {
requestHostTimeout(handleTimeout, firstTimer.startTime - currentTime);
}
}
}
}
advanceTimers
advanceTimers方法用于处理定时器队列和任务队列,具体来讲就是遍历定时器队列,将定时器队列中过期的任务按照优先级放在任务队列中。
js
function advanceTimers(currentTime) {
// 取出定时器队列中优先级最高的任务
let timer = peek(timerQueue);
while (timer != null) {
// 若定时器任务的回调为null,则调用pop方法,移除该任务
if (timer.callback === null) {
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// 若该任务的开始时间小于或等于当前时间,则说明该任务已过期,也需要移除该任务
pop(timerQueue);
// 修改任务的sortIndex为expirationTime过期时间
timer.sortIndex = timer.expirationTime;
// 将该任务放到任务队列中
push(taskQueue, timer);
} else {
// 其他情况,跳出循环
return;
}
// 从定时器队列中,取下一个优先级次高的任务
timer = peek(timerQueue);
}
}
任务队列
从上面的分析可以看出,延时任务队列的任务最终也会放入到任务队列中去进行调度执行。任务队列的调度会调用requestHostCallback方法。
requestHostCallback
js
function requestHostCallback() {
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
schedulePerformWorkUntilDeadline();
}
}
requestHostCallback的内部就是根据标志isMessageLoopRunning调用schedulePerformWorkUntilDeadline方法,而该方法设计的很巧妙。
schedulePerformWorkUntilDeadline
js
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout:null;
const localSetTimeout = typeof setImmediate !=='undefined'? setImmediate:null;
let schedulePerformWorkUntilDeadline;
if(typeof localSetImmediate === 'function'){
schedulePerformWorkUntilDeadline = ()=>{
localSetImmediate(performWorkUntilDeadline);
}
}else if(typeof MessageChannel !== 'undefined'){
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUnitDeadline=()=>{
port.postMessage(null)
}
}else{
schedulePerformWorkUntilDeadline = ()=>{
localSetTimeout(performWorkUntilDeadline,0)
}
}
Scheduler调度优先使用setImmediate,因为它不会阻止Node.js进程退出,其次考虑使用MessageChannel消息通道,它的执行时机是在当前任务完成后、渲染之前立即执行回调,且没有4ms的最小延迟,兜底方案是setTimeout定时器,该定时器在前者之后才执行,而且会有4ms的限制。
通过上述逐渐降级的兼容方案,实现了任务的异步调度。