本篇我们看下调度器部分,本系列文章旨在分析react核心模块内部运行原理和源码实现,提升架构和编码能力。
一、调度器核心功能
调度器最核心的功能就是:
- 异步渲染
- 时间切片
- 优先级调度
二、调度器(Scheduler)整体运行流程
调度器的核心目标是:根据任务优先级,合理分配浏览器资源,高优任务优先执行,低优任务可延迟 / 中断。整体流程如下:
-
提交任务 :通过
scheduleCallback(priority, callback)
提交任务,传入优先级和执行函数。 -
计算过期时间:根据优先级计算任务的「过期时间」(如用户交互任务 250ms 后过期)。
-
入队排序:任务被插入小顶堆队列,按过期时间排序,堆顶为最高优任务。
-
调度执行:
- 高优任务:同步执行,不设时间限制(确保用户操作快速响应)。
- 低优任务:请求浏览器空闲时间,每次执行不超过 5ms。
-
任务续行:若低优任务在时间切片内未完成,返回「续行回调」,下次调度时恢复执行。
-
抢占逻辑:新高优任务插入时,中断当前低优任务,优先执行高优任务。
三、核心阶段源码分析
React 调度器源码位于 scheduler
包(packages/scheduler/src/Scheduler.js
),核心阶段包括任务提交 、优先级计算 、队列管理 和任务执行。
1. 任务提交阶段(scheduleCallback
)
scheduleCallback
是调度器入口,负责创建任务并加入队列。
javascript
function unstable_scheduleCallback(priorityLevel, callback) {
// 1. 计算任务过期时间(核心:优先级转换为时间)
const currentTime = getCurrentTime(); // 当前时间(ms)
let timeout;
switch (priorityLevel) {
case ImmediatePriority: // 同步优先级(最高)
timeout = IMMEDIATE_PRIORITY_TIMEOUT; // -1
break;
case UserBlockingPriority: // 用户交互优先级(高)
timeout = USER_BLOCKING_PRIORITY_TIMEOUT; // 250ms
break;
case NormalPriority: // 正常优先级(中)
timeout = NORMAL_PRIORITY_TIMEOUT; // 5000ms
break;
case LowPriority: // 低优先级
timeout = LOW_PRIORITY_TIMEOUT; // 10000ms
break;
case IdlePriority: // 空闲优先级(最低)
timeout = IDLE_PRIORITY_TIMEOUT; // 1073741823ms(约34年)
break;
}
const expirationTime = timeout > 0 ? currentTime + timeout : timeout;
// 2. 创建任务对象
const newTask = {
id: taskIdCounter++,
callback, // 任务执行函数
priorityLevel,
expirationTime,
startTime: currentTime, // 任务开始时间
sortIndex: -1, // 排序索引(用于堆排序)
};
// 3. 按优先级插入任务队列(小顶堆)
if (enableProfiling) {
// 性能监控逻辑(略)
}
newTask.sortIndex = expirationTime; // 按过期时间排序
push(taskQueue, newTask);
// 4. 调度任务执行
if (!isHostCallbackScheduled && !isPerformingWork) {
isHostCallbackScheduled = true;
requestHostCallback(flushWork); // 请求执行任务
}
return newTask; // 返回任务标识,用于取消任务
}
核心逻辑:
- scheduleCallback和unstable_scheduleCallback是同一个函数,在协调那边调用,注册一个带优先级的performConcurrentWorkOnRoot任务。前面一篇文中react工作循环
- 将「优先级等级」转换为「过期时间」(优先级越高,过期时间越早)。
- 任务队列采用小顶堆结构,确保每次能快速取出「最早过期」的任务(最高优)。
- requestHostCallback启动一个宏任务,可以看下面的源码部分。它既然把
flushWork
作为入参,那么任务的执行者 本质上调用的就是flushWork
。flushWork也在下文中
2. 任务调度阶段(requestHostCallback
)
requestHostCallback
负责向浏览器请求执行时机,低优任务利用空闲时间,高优任务同步执行。
ini
// 简化版源码
let isMessageLoopRunning = false;
const performWorkUntilDeadline = () => {
if (scheduledHostCallback !== null) {
const hasTimeRemaining = true;
// scheduledHostCallback去执行任务的函数,
// 当任务因为时间片被打断时,它会返回true,表示
// 还有任务,所以会再让调度者调度一个执行者
// 继续执行任务
const hasMoreWork = scheduledHostCallback(
hasTimeRemaining,
currentTime,
);
if (!hasMoreWork) {
// 如果没有任务了,停止调度
isMessageLoopRunning = false;
scheduledHostCallback = null;
} else {
// 如果还有任务,继续让调度者调度执行者,便于继续
// 完成任务
port.postMessage(null);
}
} else {
isMessageLoopRunning = false;
}
};
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
requestHostCallback = function(callback) {
scheduledHostCallback = callback;
if (!isMessageLoopRunning) {
isMessageLoopRunning = true;
port.postMessage(null);
}
};
核心逻辑:
- 低优任务通过
MessageChannel
延迟执行,每次执行不超过5ms
(时间切片)。 - 若任务在时间切片内未完成,会保存状态并请求下一次执行(实现 "可中断")。
- port.postMessage(null)发起后,回调函数performWorkUntilDeadline会执行,上面scheduledHostCallback = callback被赋值,callback就是上面传入的flushWork函数。scheduledHostCallback == flushWork,所以performWorkUntilDeadline执行就是执行flushWork。
3. 任务执行阶段(flushWork
)
flushWork
是任务执行的核心函数,负责从队列中取出最高优任务并执行。
ini
// 省略无关代码
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;
}
}
// 省略部分无关代码
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;
// 执行回调
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队列没有清空, 返回ture. 等待调度中心下一次回调
} else {
return false; // task队列已经清空, 返回false.
}
}
workLoop
循环是核心,下面我们来分析下,workLoop函数中currentTask.callback其实就是performConcurrentWorkOnRoot即render阶段入口,通过深度优先去构建fiber树,每个fiber节点是一个执行单元(前一篇文章中两大循环之一的fiber树构建循环)。
每一次while
循环的退出就是一个时间切片, 深入分析while
循环的退出条件:
-
队列被完全清空: 这种情况就是很正常的情况, 一气呵成, 没有遇到任何阻碍.
-
超时检查(时间切片用尽): 分两种情况,一个是每次调度任务时,这个是在上面
workLoop
函数中,每次调度一个新任务前去做时间分片 检查;另一个是如果某个task.callback
执行时间太长(如:fiber树
很大)也会造成超时,每次构建一个工作单元fiber节点的时候也要去检查。所以两种情况都会时间片耗尽退出。我们分开分析 -
一个长任务超时,这种情况是fiber太大了,只构建了一部分,若时间切片用尽(
shouldYieldToHost()
返回true
),则暂停执行,等待下次调度。这个时候会协调器那边返回一个新的performConcurrentWorkOnRoot(保存上下文节点),上面代码const continuationCallback = callback(didUserCallbackTimeout)这时continuationCallback等于performConcurrentWorkOnRoot,这个时候任务不从队列中移除,下一帧时间切片继续从中断出执行(实现可恢复
)。 -
如果是多个任务间让出,这个就是下一帧时间切片选择堆顶任务继续执行(实现
让出主线程
,不阻塞)。 -
如果这个时候突然来一个优先级高的任务比如用户点击事件,这个时候会取消当前的正在执行的低优先级任务,先调用高优先级任务,执行完后,再重新调用低优先级任务(实现
高优先级
打断低优先级
)。
4. 时间分片
shouldYieldToHost
函数在每次任务间调度(多个短任务在一个时间片内调度)和一个长任务内(超过5ms,执行fiber构建)调用,都要判断是否需要中断。
js
const localPerformance = performance;
// 获取当前时间
getCurrentTime = () => localPerformance.now();
// 时间切片周期, 默认是5ms(如果一个task运行超过该周期, 下一个task执行之前, 会把控制权归还浏览器)
let yieldInterval = 5;
let deadline = 0;
const maxYieldInterval = 300;
let needsPaint = false;
const scheduling = navigator.scheduling;
// 是否让出主线程
shouldYieldToHost = function() {
const currentTime = getCurrentTime();
if (currentTime >= deadline) {
if (needsPaint || scheduling.isInputPending()) {
// There is either a pending paint or a pending input.
return true;
}
// There's no pending input. Only yield if we've reached the max
// yield interval.
return currentTime >= maxYieldInterval; // 在持续运行的react应用中, currentTime肯定大于300ms, 这个判断只在初始化过程中才有可能返回false
} else {
// There's still time left in the frame.
return false;
}
};
// 请求绘制
requestPaint = function() {
needsPaint = true;
};
// 设置时间切片的周期
forceFrameRate = function(fps) {
if (fps < 0 || fps > 125) {
// Using console['error'] to evade Babel and ESLint
console['error'](
'forceFrameRate takes a positive int between 0 and 125, ' +
'forcing frame rates higher than 125 fps is not supported',
);
return;
}
if (fps > 0) {
yieldInterval = Math.floor(1000 / fps);
} else {
// reset the framerate
yieldInterval = 5;
}
};
四、核心总结
本节主要分析了react调度器核心源码实现及流程, 介绍了时间切片
和可中断渲染
等特性在任务调度循环
中的实现,react实现异步可中断渲染成为现实。
-
异步:宏任务Messagechannnel实现,每个任务都是异步执行的。
-
可中断:时间分片实现,一个时间分片内可能执行多个任务或者一个长任务区别,所以任务间和任务内,都要判断是否到期,每个任务内部按照fiber单元执行,判断时间片是否到期。