React Scheduler
1 ) 概述
- react当中的异步调度,称为 React Scheduler
- 发布成单独的一个 npm 包就叫做 scheduler
- 这个包它做了什么?
- A. 首先它维护时间片
- B. 然后模拟
requestIdleCallback
这个API- 因为现在浏览器的支持不是特别的多
- 所以在浏览当中只是去模拟了一个这个API,而不是直接使用这个API
- 因为需要考虑到浏览器兼容性
- 这个API的作用
- 调用这个API传入一个回调之后,这个API会等到浏览器把它的一些主要任务执行完了
- 当它有空闲的时间的时候,再回来调用这个回调
- 相对于
requestAnimationFrame
来说,它的优先级会低很多 - 它是等浏览器器要做的事情做完了之后,再回来调这个回调
- 而
requestAnimationFrame
是浏览器要渲染当前帧的时候,调用这个回调
- C. 调度列表和进行一个超时的判断
- 关于时间片
- 不管是在浏览器还是在App当中,要给用户很流畅的一个感觉的时候
- 至少要保证在一秒钟之内要渲染30帧以上
- 现在的一些高刷新率的浏览器,可能会要求在60帧以上,甚至还有更高的,比如,120帧
- 这个帧数就是我们1秒钟,页面要重新渲染刷新多少次
- 它并不是说我一秒钟之内刷新30次,满足就行了。
- 比如前面的半秒钟只刷新了一次,后面的半秒钟刷新了二十九次,这个也是不行的
- 这个给用户的感觉,就是前面这半秒钟会特别的卡就一动不动,然后后面又变得流畅
- 所以,它的要求还需要是平均的每33毫秒要刷新1帧,要保持这个频率
- 浏览器必须自己去渲染这些动画,要每1帧里面有固定的时间去渲染这个动画
- 在这里举个例子,比如说整个应用所有的js的操作,都是通过 react 来实现的
- 而浏览器有一个一直在更新的动画, 浏览器渲染这个动画如果要11毫秒
- 那么给每一帧的, 就是把一秒钟分成了30帧之后,每一帧是33毫秒
- 这个33毫秒里面的11毫秒是必须要留给浏览器去渲染这个动画的, 才能让这个动画看起来是流畅的
- 而在这个时候留给react去渲染它的应用更新的时候,每一帧里面就只有22毫秒
- 如果react它在这一帧里面的一个更新,它需要渲染的时间很长,比如说35毫秒
- 那这个时候,我们一帧的时间就全部给react渲染给占掉了
- 因为 js 引擎是单线程的, 如果react在一直在执行,浏览器它就没有机会去获得运行权
- 就没有机会去刷新它的一个动画, 这时候,不仅把一帧的时间占完了
- 这样还不够,还要去下一帧里面借用一点时间,那么这个时间用完之后
- 浏览器要去更新动画,如果这一帧里面我们就用掉了13毫秒,剩下的时间就只剩下20毫秒
- 那么这20毫秒,又可能要运行一部分react的更新,然后再去浏览器的一个渲染
- 这就会导致整个动画变得卡顿起来了
- 这就是 React Scheduler 它的一个目的, 为了保证react它去执行更新的这个时间
- 不超过在浏览器的每一帧里面特定的时间,它希望留给浏览器去刷新动画,或者是响应用户输入的反馈的时候
- 每一帧里面有足够的时间
2 )时间片源码
-
时间片源码在 packages/scheduler 这个包里面,是一个单独的模块,单独发布到 npm 上
-
在 ReactFiberScheduler.js 里面,哪个地方用到它呢?
- 在
requestWork
函数里面,如果 expirationTime 异步的,就会调用scheduleCallbackWithExpirationTime
function scheduleCallbackWithExpirationTime(root: FiberRoot,expirationTime: ExpirationTime, ) {if (callbackExpirationTime !== NoWork) {// A callback is already scheduled. Check its expiration time (timeout).if (expirationTime > callbackExpirationTime) {// Existing callback has sufficient timeout. Exit.return;} else {if (callbackID !== null) {// Existing callback has insufficient timeout. Cancel and schedule a// new one.cancelDeferredCallback(callbackID);}}// The request callback timer is already running. Don't start a new one.} else {startRequestCallbackTimer();}callbackExpirationTime = expirationTime;const currentMs = now() - originalStartTimeMs;const expirationTimeMs = expirationTimeToMs(expirationTime);const timeout = expirationTimeMs - currentMs;callbackID = scheduleDeferredCallback(performAsyncWork, {timeout}); }
- 全局变量
callbackExpirationTime
对应的是 上一次调用 React Scheduler 去申请了一个callback - 这个callback 也会有一个 expirationTime, 因为是异步调度,所以会有一个 expirationTime 传进来
- 如果这个
callbackExpirationTime
!==NoWork
代表之前有一个callback在执行了 - 这边就会判断当前的 expirationTime 是否比之前回调中的那个要大
- 如果大,说明当前的这个的优先级要低,这个时候就直接return了不执行
- 因为它优先级更低,我们肯定要执行优先级更高的那个,调用
cancelDeferredCallback
把之前的 cancel 掉 startRequestCallbackTimer
这个函数跳过,不涉及主流程,涉及DEV Tool 相关- 接着更新一系列的变量
- 更新
callbackExpirationTime
- 计算出
timeout
- 更新
- 最后调用
scheduleDeferredCallback
这个方法来自于 ReactFiberHostConfig.js- 如果直接查找 这个文件,发现基本上没有什么内容, 是因为 React对于打包工具的配置,进行了文件名的映射
- 它实际映射的是 eact-reconciler/src/forks/ReactFiberHostConfig.dom.js
export * from 'react-dom/src/client/ReactDOMHostConfig';
- 发现里面就一行代码,找到对应的 ReactDOMHostConfig.js 文件,搜索
scheduleDeferredCallback
方法export {unstable_scheduleCallback as scheduleDeferredCallback, } from 'scheduler';
- 可追溯到 这个方法来自于 scheduler 包
- 这个方法涉及比较多,先跳过
callbackID = scheduleDeferredCallback(performAsyncWork, {timeout});
- 它最后返回 一个 callbackID, 这个id用于后期 cancel 的标识,
cancelDeferredCallback(callbackID);
- 这里之前也说了,如果新的任务优先级更高,需要把老的取消,再调用新的callback
- 而里面的参数
performAsyncWork
- 在
requestWork
中,当 expirationTime === Sync 时,调用的也是performSyncWork
这个是同步的 - 而如果是异步,则调用
scheduleCallbackWithExpirationTime
函数,最终调用的是这里的performAsyncWork
- 所以,这两个是对应的,同步和异步
- 在
- 全局变量
- 在
-
进入 scheduleDeferredCallback 函数的源码 packages/scheduler/src/Scheduler.js 找到
unstable_scheduleCallback
function unstable_scheduleCallback(callback, deprecated_options) {var startTime =currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();var expirationTime;if (typeof deprecated_options === 'object' &&deprecated_options !== null &&typeof deprecated_options.timeout === 'number') {// FIXME: Remove this branch once we lift expiration times out of React.expirationTime = startTime + deprecated_options.timeout;} else {switch (currentPriorityLevel) {case ImmediatePriority:expirationTime = startTime + IMMEDIATE_PRIORITY_TIMEOUT;break;case UserBlockingPriority:expirationTime = startTime + USER_BLOCKING_PRIORITY;break;case IdlePriority:expirationTime = startTime + IDLE_PRIORITY;break;case NormalPriority:default:expirationTime = startTime + NORMAL_PRIORITY_TIMEOUT;}}var newNode = {callback,priorityLevel: currentPriorityLevel,expirationTime,next: null,previous: null,};// Insert the new callback into the list, ordered first by expiration, then// by insertion. So the new callback is inserted any other callback with// equal expiration.if (firstCallbackNode === null) {// This is the first callback in the list.firstCallbackNode = newNode.next = newNode.previous = newNode;ensureHostCallbackIsScheduled();} else {var next = null;var node = firstCallbackNode;do {if (node.expirationTime > expirationTime) {// The new callback expires before this one.next = node;break;}node = node.next;} while (node !== firstCallbackNode);if (next === null) {// No callback with a later expiration was found, which means the new// callback has the latest expiration in the list.next = firstCallbackNode;} else if (next === firstCallbackNode) {// The new callback has the earliest expiration in the entire list.firstCallbackNode = newNode;ensureHostCallbackIsScheduled();}var previous = next.previous;previous.next = next.previous = newNode;newNode.next = next;newNode.previous = previous;}return newNode; }
- 首先看 参数
callback, deprecated_options
- callback 是传进来的
performAsyncWork
- deprecated_options 是即将被废弃的 optinos,这个即将被废弃
- callback 是传进来的
- 接着处理
var startTime = currentEventStartTime !== -1 ? currentEventStartTime : getCurrentTime();
getCurrentTime
是重新计算一个 xx.now()if (hasNativePerformanceNow) {var Performance = performance;getCurrentTime = function() {return Performance.now();}; } else {getCurrentTime = function() {return localDate.now();}; }
- 这里,浏览器平台是这个
localDate.now();
- 下面有个判断
if (typeof deprecated_options === 'object' && deprecated_options !== null && typeof deprecated_options.timeout === 'number')
- 接着判断 deprecated_options 这个参数,存在则计算出 expirationTime
// FIXME: Remove this branch once we lift expiration times out of React. expirationTime = startTime + deprecated_options.timeout;
- 当把 expirationTime 相关的逻辑提取出来之后,这个 if判断就被删除了,后面只有 else 里面的东西了
- 所以说,这个
deprecated_options
即将被废弃
- 接着判断 deprecated_options 这个参数,存在则计算出 expirationTime
- 如果走到 else 里面,进行switch case
currentPriorityLevel
- 可以看下各个常量的值
var maxSigned31BitInt = 1073741823;// Times out immediately var IMMEDIATE_PRIORITY_TIMEOUT = -1; // Eventually times out var USER_BLOCKING_PRIORITY = 250; var NORMAL_PRIORITY_TIMEOUT = 5000; // Never times out var IDLE_PRIORITY = maxSigned31BitInt;
- 也就是说,将来很可能会把 expirationTime 相关逻辑移入 scheduler 包中
- 之前在 packages/react-reconciler/src/ReactFiberReconciler.js 中
- 不过,在目前的逻辑中 else 里面的东西,用不到
- 可以看下各个常量的值
- 接下去,创建 newNode 的对象
var newNode = {callback,priorityLevel: currentPriorityLevel,expirationTime,next: null,previous: null, };
- next 和 previous 是用来存储链表的数据结构的
- 接下来
if (firstCallbackNode === null)
- firstCallbackNode 是 scheduler 中维护的一个单项列表的头部
- 如果匹配判断,说明传递进来的 callback 是第一个
- 进行赋值处理
firstCallbackNode = newNode.next = newNode.previous = newNode;
- 并调用
ensureHostCallbackIsScheduled();
- 进行赋值处理
- 不匹配的时候
- 有一个或多个callback, 则进行循环
- 在循环中判断,
node.expirationTime > expirationTime
- 如果匹配,
next = node;
并跳出循环 - 这是 scheduler 对于传进来的所有callback, 按照 expirationTime 的大小,也就是优先级的高低进行排序
- 它会把优先级更高的任务,排到最前面
- 如果匹配,
- 如果 next 是 null
- 这个节点要插在callbackList里面的最后一个
- 如果 next 是 firstCallbackNode,即第一个
- 因为当前节点要插在这个单项列表最前面,优先级最高
- 马上 firstCallbackNode 变化了,即更新了
firstCallbackNode = newNode;
- 调用
ensureHostCallbackIsScheduled();
- 这个函数在上面两处调用了,但是没有在
if (next === null)
中调用- 因为 这个条件下,firstCallbackNode 仍然处于第一位
- 后续要调用的话,第一个被调用的还是 firstCallbackNode
- 所以,顺序不会变,所以不需要重新调用
ensureHostCallbackIsScheduled();
- 注意,调用上述方法会进入一个循环,循环的调用List里面的东西
- 当 firstCallbackNode 变化了,才会去调用,因为头部变了
- 这个函数在上面两处调用了,但是没有在
- 首先看 参数
-
下面为这个方法链表的处理示例