您现在的位置是:首页 >学无止境 >React源码解读(一)scheduler.js网站首页学无止境
React源码解读(一)scheduler.js
react/packages/scheduler/src/forks/Scheduler.js
这个文件主要是 React 调度器(Scheduler)的实现代码,用于管理任务的调度和执行,确保不同优先级的任务能够在合适的时机被处理。以下是对该文件中关键部分的详细介绍:
1. 任务(Task)类型定义
export opaque type Task = {
id: number,
callback: Callback | null,
priorityLevel: PriorityLevel,
startTime: number,
expirationTime: number,
sortIndex: number,
isQueued?: boolean,
};
id
:任务的唯一标识符,通过taskIdCounter
递增生成,用于维护任务的插入顺序。callback
:任务要执行的回调函数,可能为null
,表示任务已被取消。priorityLevel
:任务的优先级,如ImmediatePriority
、UserBlockingPriority
等。startTime
:任务的开始时间。expirationTime
:任务的过期时间,用于判断任务是否超时。sortIndex
:任务在队列中的排序索引,用于在堆中排序。isQueued
:可选属性,用于标记任务是否已入队,在性能分析时使用。
PriorityLevel
是在 react/packages/scheduler/src/SchedulerPriorities.js
文件中定义的类型,具体内容如下:
export type PriorityLevel = 0 | 1 | 2 | 3 | 4 | 5;
// TODO: Use symbols?
export const NoPriority = 0;
export const ImmediatePriority = 1;
export const UserBlockingPriority = 2;
export const NormalPriority = 3;
export const LowPriority = 4;
export const IdlePriority = 5;
PriorityLevel
是一个联合类型,它的值可以是 0
、1
、2
、3
、4
或 5
。同时,为了方便使用,还定义了一些常量来表示这些优先级:
NoPriority
:值为0
,表示无优先级。ImmediatePriority
:值为1
,表示立即执行的最高优先级。UserBlockingPriority
:值为2
,表示用户阻塞优先级,通常用于需要立即响应用户操作的任务。NormalPriority
:值为3
,表示普通优先级。LowPriority
:值为4
,表示低优先级。IdlePriority
:值为5
,表示空闲优先级,通常用于在浏览器空闲时执行的任务。
2. 获取当前时间的函数
let getCurrentTime: () => number | DOMHighResTimeStamp;
const hasPerformanceNow =
// $FlowFixMe[method-unbinding]
typeof performance === 'object' && typeof performance.now === 'function';
if (hasPerformanceNow) {
const localPerformance = performance;
getCurrentTime = () => localPerformance.now();
} else {
const localDate = Date;
const initialTime = localDate.now();
getCurrentTime = () => localDate.now() - initialTime;
}
- 首先检查浏览器是否支持
performance.now()
方法,如果支持,则使用该方法获取当前时间。 - 如果不支持,则使用
Date.now()
方法,并通过减去初始时间来模拟相对时间。
3. 最大 31 位整数常量
// Max 31 bit integer. The max integer size in V8 for 32-bit systems.
// Math.pow(2, 30) - 1
// 0b111111111111111111111111111111
var maxSigned31BitInt = 1073741823;
- 定义了一个最大 31 位整数常量
maxSigned31BitInt
,用于表示 V8 引擎在 32 位系统中的最大整数大小。
4. 任务队列和定时器队列
// Tasks are stored on a min heap
var taskQueue: Array<Task> = [];
var timerQueue: Array<Task> = [];
taskQueue
:存储即将执行的任务,使用最小堆(min heap)数据结构进行排序,确保优先级高的任务先执行。timerQueue
:存储延迟执行的任务,同样使用最小堆排序,根据任务的开始时间进行排序。
5. 任务计数器和当前任务信息
// Incrementing id counter. Used to maintain insertion order.
var taskIdCounter = 1;
var currentTask = null;
var currentPriorityLevel = NormalPriority;
// This is set while performing work, to prevent re-entrance.
var isPerformingWork = false;
var isHostCallbackScheduled = false;
var isHostTimeoutScheduled = false;
var needsPaint = false;
taskIdCounter
:任务的 ID 计数器,每次创建新任务时递增,用于生成唯一的任务 ID。currentTask
:当前正在执行的任务。currentPriorityLevel
:当前的优先级级别,默认为NormalPriority
。isPerformingWork
:标志位,用于表示是否正在执行任务,防止重入。isHostCallbackScheduled
:标志位,表示是否已经调度了宿主回调。isHostTimeoutScheduled
:标志位,表示是否已经调度了宿主超时。needsPaint
:标志位,表示是否需要进行重绘。
6. 本地 API 引用
// Capture local references to native APIs, in case a polyfill overrides them.
const localSetTimeout = typeof setTimeout === 'function' ? setTimeout : null;
const localClearTimeout =
typeof clearTimeout === 'function' ? clearTimeout : null;
const localSetImmediate =
typeof setImmediate !== 'undefined' ? setImmediate : null; // IE and Node.js + jsdom
- 捕获本地的
setTimeout
、clearTimeout
和setImmediate
函数的引用,以防这些函数被 polyfill 覆盖。
7. 推进定时器函数
function advanceTimers(currentTime: number) {
// Check for tasks that are no longer delayed and add them to the queue.
let timer = peek(timerQueue);
while (timer !== null) {
if (timer.callback === null) {
// Timer was cancelled.
pop(timerQueue);
} else if (timer.startTime <= currentTime) {
// Timer fired. Transfer to the task queue.
pop(timerQueue);
timer.sortIndex = timer.expirationTime;
push(taskQueue, timer);
if (enableProfiling) {
markTaskStart(timer, currentTime);
timer.isQueued = true;
}
} else {
// Remaining timers are pending.
return;
}
timer = peek(timerQueue);
}
}
- 该函数用于检查定时器队列中是否有任务已经达到开始时间,如果有,则将其从定时器队列中移除,并添加到任务队列中。
- 如果任务已经被取消(
callback
为null
),则直接从定时器队列中移除。 - 如果任务的开始时间小于等于当前时间,则将其从定时器队列中移除,并添加到任务队列中,同时更新任务的排序索引。
- 如果任务的开始时间大于当前时间,则表示剩余的定时器任务仍在等待,直接返回。
总的来说,这个文件通过定义任务类型、管理任务队列和定时器队列,以及实现一些辅助函数,实现了一个任务调度器,用于管理 React 中的异步任务。
在 React 中,任务调度是一个非常重要的机制,它允许 React 有效地管理和执行不同优先级的任务,以确保用户界面的流畅性和响应性。以下是基于你提供的代码片段及 React 的整体设计来详细描述 React 任务调度过程:
1. 初始化和状态设置
首先,会有一些初始化的操作,比如设置一些标志位。在你提供的代码片段中,有 needsPaint = false;
这样的操作,它可能用于标记是否需要进行重绘。
2. 检查消息循环状态
通过 isMessageLoopRunning
这个标志来判断消息循环是否正在运行。如果正在运行,会执行以下操作:
if (isMessageLoopRunning) {
const currentTime = getCurrentTime();
// Keep track of the start time so we can measure how long the main thread
// has been blocked.
startTime = currentTime;
这里会获取当前时间 currentTime
并记录为 startTime
,目的是为了后续测量主线程被阻塞的时长。
3. 执行任务
在消息循环运行的情况下,会尝试执行任务。这里会使用 flushWork
函数来执行具体的工作:
let hasMoreWork = true;
try {
hasMoreWork = flushWork(currentTime);
} finally {
if (hasMoreWork) {
// If there's more work, schedule the next message event at the end
// of the preceding one.
schedulePerformWorkUntilDeadline();
} else {
isMessageLoopRunning = false;
}
}
flushWork
函数会执行当前的任务,并且返回一个布尔值hasMoreWork
表示是否还有更多的任务需要执行。- 如果
hasMoreWork
为true
,说明还有任务需要执行,那么会调用schedulePerformWorkUntilDeadline
函数来安排下一次的任务执行。 - 如果
hasMoreWork
为false
,则将isMessageLoopRunning
标志设置为false
,表示消息循环结束。
4. 调度下一次任务执行
schedulePerformWorkUntilDeadline
函数的实现会根据不同的环境有所不同:
let schedulePerformWorkUntilDeadline;
if (typeof localSetImmediate === 'function') {
// Node.js and old IE.
schedulePerformWorkUntilDeadline = () => {
localSetImmediate(performWorkUntilDeadline);
};
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
- 在 Node.js 和旧版 IE 环境中,如果
localSetImmediate
函数存在,会使用localSetImmediate
来调度下一次的任务执行。 - 在 DOM 和 Worker 环境中,如果
MessageChannel
可用,会使用MessageChannel
来调度下一次的任务执行。
在这段代码中,当处于 DOM 和 Worker 环境且 MessageChannel
可用时,会使用 MessageChannel
来调度下一次的任务执行。下面详细解释这部分代码的逻辑和原因:
代码片段
} else if (typeof MessageChannel !== 'undefined') {
// DOM and Worker environments.
// We prefer MessageChannel because of the 4ms setTimeout clamping.
const channel = new MessageChannel();
const port = channel.port2;
channel.port1.onmessage = performWorkUntilDeadline;
schedulePerformWorkUntilDeadline = () => {
port.postMessage(null);
};
}
详细解释
-
条件判断:
} else if (typeof MessageChannel !== 'undefined') {
这里使用
typeof MessageChannel !== 'undefined'
来检查当前环境是否支持MessageChannel
。如果支持,说明当前环境可能是 DOM 或 Worker 环境,因为MessageChannel
主要在这些环境中可用。 -
创建
MessageChannel
实例:const channel = new MessageChannel(); const port = channel.port2;
MessageChannel
是 HTML5 中引入的一个用于在不同上下文(如主线程和 Web Worker 之间)进行通信的 API。- 当创建一个
MessageChannel
实例时,它会有两个端口port1
和port2
,这两个端口可以互相通信。这里将port2
赋值给变量port
,后续会使用这个端口来发送消息。
-
设置消息监听:
channel.port1.onmessage = performWorkUntilDeadline;
- 给
port1
注册了一个onmessage
事件处理函数performWorkUntilDeadline
。这意味着当port2
发送消息时,port1
会接收到这个消息,并触发performWorkUntilDeadline
函数的执行。
- 给
-
定义调度函数:
schedulePerformWorkUntilDeadline = () => { port.postMessage(null); };
schedulePerformWorkUntilDeadline
是一个函数,用于调度performWorkUntilDeadline
的执行。- 当调用
schedulePerformWorkUntilDeadline
时,它会通过port.postMessage(null)
向port1
发送一个消息(这里的消息内容为null
,只是一个触发信号)。 - 由于
port1
监听了onmessage
事件,当收到消息后,就会执行performWorkUntilDeadline
函数,从而完成任务的调度。
使用 MessageChannel
的原因
// We prefer MessageChannel because of the 4ms setTimeout clamping.
注释中提到了使用 MessageChannel
的原因是因为 setTimeout
的 4ms 限制。在浏览器环境中,setTimeout
有一个最小延迟时间的限制,通常是 4ms(不同浏览器可能略有不同)。这意味着如果使用 setTimeout
来调度任务,即使设置的延迟时间为 0,实际的延迟也可能会达到 4ms 左右。而使用 MessageChannel
可以避免这个问题,能够更精确地控制任务的调度时间,从而提高性能。
综上所述,在 DOM 和 Worker 环境中,如果 MessageChannel
可用,通过创建 MessageChannel
实例并利用其端口之间的消息传递机制,可以更精确地调度下一次的任务执行。
5. 任务优先级管理
在 React 中,不同的任务会有不同的优先级。比如,用户交互相关的任务(如
点击事件)通常会有较高的优先级,而一些数据更新或渲染任务可能会有较低的优先级。React 会根据任务的优先级来决定哪些任务先执行,哪些任务后执行。
6. 空闲时间利用
React 会利用浏览器的空闲时间来执行一些低优先级的任务。当主线程没有其他高优先级任务需要处理时,React 会尝试执行这些低优先级的任务,以提高性能和用户体验。
总的来说,React 的任务调度过程是一个复杂而高效的机制,它通过不断地检查消息循环状态、执行任务、调度下一次任务执行,并根据任务的优先级来管理任务的执行顺序,从而确保用户界面的流畅性和响应性。