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
文章来源:https://www.toymoban.com/news/detail-799621.htmlfunction 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 变化了,才会去调用,因为头部变了
- 这个函数在上面两处调用了,但是没有在
- 首先看 参数
-
下面为这个方法链表的处理示例文章来源地址https://www.toymoban.com/news/detail-799621.html
- 接着,进入
ensureHostCallbackIsScheduled
这个方法让队列进入调度的过程function ensureHostCallbackIsScheduled() { if (isExecutingCallback) { // Don't schedule work yet; wait until the next time we yield. return; } // Schedule the host callback using the earliest expiration in the list. var expirationTime = firstCallbackNode.expirationTime; if (!isHostCallbackScheduled) { isHostCallbackScheduled = true; } else { // Cancel the existing host callback. cancelHostCallback(); } requestHostCallback(flushWork, expirationTime); }
-
isExecutingCallback
变量表示已经调用callback, 直接 return- 代表着 已经有一个callbackNode 被调用了
- 也就是我们传入的
performAsyncWork
正在被调用了 - 进入被调用的过程,自动进入一个循环的过程
- 就不需要再重新启动一次调度
- 获取变量
var expirationTime = firstCallbackNode.expirationTime;
- 如果没有被调度,标识正在被调度
isHostCallbackScheduled = true;
- 否则,取消之前的回调
cancelHostCallback();
- 最后
requestHostCallback
, 进入这个方法,有很多种场景分别定义,但是找到我们需要的场景,搜索该方法名- 排除 mock, 非浏览器环境的判断
- 并进入直接到 else 中
// 这里跳过很多代码 // ... // 主要在这里 requestHostCallback = function(callback, absoluteTimeout) { scheduledHostCallback = callback; timeoutTime = absoluteTimeout; if (isFlushingHostCallback || absoluteTimeout < 0) { // Don't wait for the next frame. Continue working ASAP, in a new event. window.postMessage(messageKey, '*'); } else if (!isAnimationFrameScheduled) { // If rAF didn't already schedule one, we need to schedule a frame. // TODO: If this rAF doesn't materialize because the browser throttles, we // might want to still have setTimeout trigger rIC as a backup to ensure // that we keep performing work. isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } }; cancelHostCallback = function() { scheduledHostCallback = null; isMessageEventScheduled = false; timeoutTime = -1; };
- 上述
requestHostCallback
是我们需要关注的点-
cheduledHostCallback = callback;
读取 callback -
timeoutTime = absoluteTimeout;
是我们传进来的 expirationTime - 接着判断
if (isFlushingHostCallback || absoluteTimeout < 0)
- 这两种情况,不需要等待下一帧去做这个事情
- 而是以最快的速度进入这个方法的调用
window.postMessage(messageKey, '*');
-
absoluteTimeout < 0
说明已经超时了
- 不符合上述条件,按照正常的调度流程去走
- 判断
isAnimationFrameScheduled
这个变量的状态 - 如果它没有设置为 true, 则还没有进入调度循环的过程
- 这时候就把它设置为 true, 并执行
requestAnimationFrameWithTimeout(animationTick);
- 进入
requestAnimationFrameWithTimeout
var requestAnimationFrameWithTimeout = function(callback) { // schedule rAF and also a setTimeout rAFID = localRequestAnimationFrame(function(timestamp) { // cancel the setTimeout localClearTimeout(rAFTimeoutID); callback(timestamp); }); rAFTimeoutID = localSetTimeout(function() { // cancel the requestAnimationFrame localCancelAnimationFrame(rAFID); callback(getCurrentTime()); // 这里 getCurrentTime 是一个模拟 timestamp 的参数 }, ANIMATION_FRAME_TIMEOUT); };
- 这里的
localRequestAnimationFrame
相当于 window.requestAnimationFrame- 它内部做了两件事,清理 timeout, 执行callback
- 这个 callback 就是我们传进来的
animationTick
这个方法 -
rAFTimeoutID
是下面的 timeout 定时器 - 这个定时器的作用是: 如果
localRequestAnimationFrame
一直没有调用,超时了,这边设置的时间是 100ms - 超时后,取消
localRequestAnimationFrame
的调用,并且直接调用callback(getCurrentTime());
- 也就是下一帧的时间必须在 100ms之内被调用
- 这个方法的作用就是,防止
localRequestAnimationFrame
太长时间没有被调用 - 里面有相互取消的操作,这里面有一个竞争关系,谁先触发,谁先调用
- 这里的
- 同样,参数这里
animationTick
也是个方法var animationTick = function(rafTime) { // 这里会匹配到 if (scheduledHostCallback !== null) { // Eagerly schedule the next animation callback at the beginning of the // frame. If the scheduler queue is not empty at the end of the frame, it // will continue flushing inside that callback. If the queue *is* empty, // then it will exit immediately. Posting the callback at the start of the // frame ensures it's fired within the earliest possible frame. If we // waited until the end of the frame to post the callback, we risk the // browser skipping a frame and not firing the callback until the frame // after that. // 因为 firstCallbackNode 是一个队列,里面会有很多 callback // 当前 animationTick 只执行一个 callback // 如果后续还有,也会在下一帧中去执行 // 不期望等待callback执行完成后,再去请求下一帧,可能会跳过很多的时间 // 所以在这里立马执行 requestAnimationFrameWithTimeout(animationTick); // 立即进行调用,请求下一帧 } else { // No pending work. Exit. // 如果下次进来,scheduledHostCallback 是没有的,则跳出 isAnimationFrameScheduled = false; return; } // rafTime 是 animationTick 被调用的时间 // frameDeadline 默认是 0 // activeFrameTime 是 33,这个就是保持浏览器30帧的执行时间 // 这里就是计算,这个方法到下一帧可以执行的时间 var nextFrameTime = rafTime - frameDeadline + activeFrameTime; if ( nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime ) { if (nextFrameTime < 8) { // Defensive coding. We don't support higher frame rates than 120hz. // If the calculated frame time gets lower than 8, it is probably a bug. nextFrameTime = 8; } // If one frame goes long, then the next one can be short to catch up. // If two frames are short in a row, then that's an indication that we // actually have a higher frame rate than what we're currently optimizing. // We adjust our heuristic dynamically accordingly. For example, if we're // running on 120hz display or 90hz VR display. // Take the max of the two in case one of them was an anomaly due to // missed frame deadlines. activeFrameTime = nextFrameTime < previousFrameTime ? previousFrameTime : nextFrameTime; } else { previousFrameTime = nextFrameTime; } frameDeadline = rafTime + activeFrameTime; if (!isMessageEventScheduled) { isMessageEventScheduled = true; window.postMessage(messageKey, '*'); } };
- 第一次计算的
nextFrameTime
其实是没有用的,因为算出来的时间会比较大 - 第二次进来,这时候
frameDeadline
就不是 0 了 - 而
requestAnimationFrameWithTimeout
又是连续调用的 - 因为我们进入这个方法就会立马调用这个方法
- 下个方法调用就是下一帧了,因为 requestAnimationFrame 是一帧一帧来调用的
- 下一帧时间进来,又重新计算出来了一个
nextFrameTime
- 这个时候,rafTime 是小于 frameDeadline 的,因为 frameDeadline 加上了一个完整帧的时间 33
- 对于调用 requestAnimationFrame 的时候,是下一帧动画刚开始渲染的时候,肯定没有到 33 毫秒的时候
- 这时候
nextFrameTime
是小于 33,说明机器的刷新频率高于30帧 if (nextFrameTime < activeFrameTime && previousFrameTime < activeFrameTime)
- 这个判断的意义在于,如果连续两帧的调用都计算出来,发现小于 33 ms (目前的帧时间)
- 那么就把帧时间 activeFrameTime 变小,因为使用 frameDeadline 的时候,activeFrameTime
- 是非常重要的,
frameDeadline = rafTime + activeFrameTime;
- 说明在接下去的 33ms之内都是可以运行react更新的代码
- 实际浏览器的刷新时间都要小于33ms, 比如 10ms, 这时候,占用33ms去渲染react应用
- 就会导致浏览器刷新动画的时间,非常不够,就导致动画变得比较卡顿
- 这个是考虑不同平台刷新频率的问题,不如 VR平台对刷新要求比较高
- 如果
nextFrameTime < 8
这时候nextFrameTime = 8
- 这说明react目前不支持每帧小于8ms的场景
- 通过以上前后几次帧时间的判断,来判断平台的刷新频率来更新
activeFrameTime
- 来减少 react 运行时间的目的
- 但是
frameDeadline = rafTime + activeFrameTime;
这里计算出的 frameDeadline 要大于33的- 因为 activeFrameTime 是完整的一帧时间 33
- 而每帧留给 react 更新的时间要小于 33
- 一帧之内要处理 react的渲染 和 浏览器的更新,那么 react渲染一定要小于33
这里算出的 frameDeadline 是 当前时间 + 33
- 这是为什么呢?
- 这里用了js中任务队列的概念,像是 setTimeout, window.postMessage
- 都是把一个任务推到了一个队列里面, 然后再继续执行当前 js 的任务
- 对于浏览器来说,animationTick 是在 requestAnimationFrameWithTimeout 的callback中调用
- animationTick 方法执行完之后,立马进入浏览器动画刷新的流程
- 下面调用的 window.postMessage 要等到浏览器动画或用户反馈执行完了之后,才会执行 postMessage 的功能
- 这意味着需要等到浏览器刷新完成后,才会接收到 postMessage 的意图
- 这时候浏览器刷新动画的时间已经过了,相当于 rafTime + activeFrameTime 的时间已经流失掉一部分了(浏览器刷新需时)
- 剩下的时间给 react 执行更新的
- 这就是 react scheduler 中模拟 requestIdleCallback 的方法,通过 requestAnimationFrame 调用完 callback 之后
- 立马进入浏览器的动画更新的设定,在下面的判断中给任务队列插入一个任务
-
if (!isMessageEventScheduled)
isMessageEventScheduled = true;
window.postMessage(messageKey, '*');
- 在浏览器执行完之后,调用任务队列, 这个时间总共加起来是 33ms
- 当发送完 postMessage 到了哪里?可看到
window.addEventListener('message', idleTick, false)
- 进入 idleTick
var idleTick = function(event) { // 先判断 key if (event.source !== window || event.data !== messageKey) { return; } isMessageEventScheduled = false; // 赋值一份 callback var prevScheduledCallback = scheduledHostCallback; // 同样处理 timeout var prevTimeoutTime = timeoutTime; // 重置下面两个 scheduledHostCallback = null; timeoutTime = -1; // 获取当前时间 var currentTime = getCurrentTime(); var didTimeout = false; // 这个条件如果 <= 0 说明浏览器动画或用户反馈超过 33ms, 意思是,把这一帧的时间已经用完了 // 对于 react 来说,它已经没有时间执行它的更新了 if (frameDeadline - currentTime <= 0) { // There's no time left in this idle period. Check if the callback has // a timeout and whether it's been exceeded. // 进入上述条件,它需要继续判断 timeout 是否已经过期,或者小于当前时间(说明任务也已经过期了) // 如果任务已经过期,这个任务就需要强行被更新 // 可以在任务没有过期的时候,判断帧时间如果没有了,即: frameDeadline - currentTime <= 0 // 先跳过,等下一帧来更新,但是在任务已经过期的时候,就需要强制执行了,于是就设置了下面的 didTimeout = true; if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) { // Exceeded the timeout. Invoke the callback even though there's no // time left. didTimeout = true; } else { // No timeout. // 没有过期,并且 isAnimationFrameScheduled === false 去调用 requestAnimationFrameWithTimeout 这个方法 if (!isAnimationFrameScheduled) { // Schedule another animation callback so we retry later. // 恢复 isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } // Exit without invoking the callback. scheduledHostCallback = prevScheduledCallback; timeoutTime = prevTimeoutTime; return; } } // 接来下如果 存在 prevScheduledCallback 则设置 isFlushingHostCallback 并调用 prevScheduledCallback if (prevScheduledCallback !== null) { isFlushingHostCallback = true; try { prevScheduledCallback(didTimeout); } finally { isFlushingHostCallback = false; } } };
- 这里最后的
prevScheduledCallback
向上溯源,找到ensureHostCallbackIsScheduled
function ensureHostCallbackIsScheduled() { if (isExecutingCallback) { // Don't schedule work yet; wait until the next time we yield. return; } // Schedule the host callback using the earliest expiration in the list. var expirationTime = firstCallbackNode.expirationTime; if (!isHostCallbackScheduled) { isHostCallbackScheduled = true; } else { // Cancel the existing host callback. cancelHostCallback(); } requestHostCallback(flushWork, expirationTime); }
- 可以看到这里
requestHostCallback(flushWork, expirationTime);
传入了flushWork
方法- 输出任务需要调用 flushWork 方法
- 现在来看下
flushWork
方法, 这是 react scheduler 调度到 要执行 callback 的流程 - 在执行callback的时候,调用了 flushWork 这个方法
// didTimeout 参数是 firstCallbackNode 的 expirationTime 是否已超时 function flushWork(didTimeout) { // 真正调用 callback 设置为 true // 对于 ensureHostCallbackIsScheduled 方法来说,如果为 true, 则直接 return 了 isExecutingCallback = true; deadlineObject.didTimeout = didTimeout; // deadlineObject 是上层设置的一个通用的对象 try { if (didTimeout) { // Flush all the expired callbacks without yielding. while (firstCallbackNode !== null) { // Read the current time. Flush all the callbacks that expire at or // earlier than that time. Then read the current time again and repeat. // This optimizes for as few performance.now calls as possible. var currentTime = getCurrentTime(); if (firstCallbackNode.expirationTime <= currentTime) { // 执行 callbackNode 的链表直到遇到第一个不过期的为止,把已过期的任务都强制输出 do { flushFirstCallback(); } while ( firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime ); // 这里 firstCallbackNode 是 next, firstCallbackNode.expirationTime <= currentTime 这表示下一个节点的任务还是过期的任务 continue; // 这里continue 跳出后即 break 跳出 外层while循环 } break; } } else { // 这里表示没有任务是过期的 // Keep flushing callbacks until we run out of time in the frame. if (firstCallbackNode !== null) { do { flushFirstCallback(); } while ( firstCallbackNode !== null && getFrameDeadline() - getCurrentTime() > 0 ); // getFrameDeadline() - getCurrentTime() > 0 表示 有空,闲暇 会执行 flushFirstCallback } } } finally { isExecutingCallback = false; // 最后,如果还有,再次进入调度 if (firstCallbackNode !== null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(); } else { isHostCallbackScheduled = false; } // Before exiting, flush all the immediate work that was scheduled. flushImmediateWork(); } }
- 关于这里的
deadlineObject
var deadlineObject = { timeRemaining, didTimeout: false, };
- 而这里的
timeRemaining
是一个方法
var timeRemaining; if (hasNativePerformanceNow) { timeRemaining = function() { // 这个判断不成立,跳过 if ( firstCallbackNode !== null && firstCallbackNode.expirationTime < currentExpirationTime ) { // A higher priority callback was scheduled. Yield so we can switch to // working on that. return 0; } // We assume that if we have a performance timer that the rAF callback // gets a performance timer value. Not sure if this is always true. // getFrameDeadline() 方法就是 frameDeadline = rafTime + activeFrameTime; // 也就是说,确定,这一帧的渲染时间,是否已经超过 var remaining = getFrameDeadline() - performance.now(); return remaining > 0 ? remaining : 0; }; } else { timeRemaining = function() { // Fallback to Date.now() if ( firstCallbackNode !== null && firstCallbackNode.expirationTime < currentExpirationTime ) { return 0; } var remaining = getFrameDeadline() - Date.now(); return remaining > 0 ? remaining : 0; }; }
- 这里根据
hasNativePerformanceNow
来进行一个区分,这两个基本差不多,选择其一 - 所以,timeRemaining 用于计算还剩多少时间,比如在 ReactFiberSchedler.js中的
shouldYield
// When working on async work, the reconciler asks the renderer if it should // yield execution. For DOM, we implement this with requestIdleCallback. function shouldYield() { if (deadlineDidExpire) { return true; } if ( deadline === null || deadline.timeRemaining() > timeHeuristicForUnitOfWork ) { // Disregard deadline.didTimeout. Only expired work should be flushed // during a timeout. This path is only hit for non-expired work. return false; } deadlineDidExpire = true; return true; }
- 这里
deadline.timeRemaining() > timeHeuristicForUnitOfWork
, 这里timeHeuristicForUnitOfWork
是 1 - 用剩下时间是否 > 1 来判断是否已经过期了,如果 > 1 说明还有时间执行 react的更新,这里 return false
- 如果剩下时间 < 1 了,代表这一帧的渲染时间已经超时,设置全局变量
deadlineDidExpire = true;
并且 return true - 这个 shouldYield 方法就是判断这个任务要跳出还是继续执行下去
- 这里
- 接着是一个 try finally 里面
-
if (didTimeout)
这时候已经过期了,进入while循环while (firstCallbackNode !== null)
- 里面 有个if 判断是肯定匹配的,
if (firstCallbackNode.expirationTime <= currentTime)
- 执行 do while
- flushFirstCallback 是真正调用callback的方法
function flushFirstCallback() { var flushedNode = firstCallbackNode; // Remove the node from the list before calling the callback. That way the // list is in a consistent state even if the callback throws. var next = firstCallbackNode.next; // 说明链表里只有一个节点,直接设置 null if (firstCallbackNode === next) { // This is the last callback in the list. firstCallbackNode = null; next = null; } else { // 如果不是,多个节点,则构建链表(环形链表) var lastCallbackNode = firstCallbackNode.previous; firstCallbackNode = lastCallbackNode.next = next; // firstCallbackNode 变成了 next 的节点 对应上面调用方法的 do while firstCallbackNode.exirationTime <= currentTime next.previous = lastCallbackNode; } // 把之前的指向清空,如果指针还留着,可能会导致问题 flushedNode.next = flushedNode.previous = null; // Now it's safe to call the callback. var callback = flushedNode.callback; var expirationTime = flushedNode.expirationTime; var priorityLevel = flushedNode.priorityLevel; var previousPriorityLevel = currentPriorityLevel; var previousExpirationTime = currentExpirationTime; currentPriorityLevel = priorityLevel; currentExpirationTime = expirationTime; var continuationCallback; try { // 这里 callback 就是传进来的 performAsyncWork continuationCallback = callback(deadlineObject); // 这里应该是 undefined } finally { currentPriorityLevel = previousPriorityLevel; currentExpirationTime = previousExpirationTime; } // A callback may return a continuation. The continuation should be scheduled // with the same priority and expiration as the just-finished callback. // 因为 performAsyncWork 这个 callback 没有返回值 所以这个目前来说不成立,也许后续会有用 if (typeof continuationCallback === 'function') { var continuationNode: CallbackNode = { callback: continuationCallback, priorityLevel, expirationTime, next: null, previous: null, }; // Insert the new callback into the list, sorted by its expiration. This is // almost the same as the code in `scheduleCallback`, except the callback // is inserted into the list *before* callbacks of equal expiration instead // of after. if (firstCallbackNode === null) { // This is the first callback in the list. firstCallbackNode = continuationNode.next = continuationNode.previous = continuationNode; } else { var nextAfterContinuation = null; var node = firstCallbackNode; do { if (node.expirationTime >= expirationTime) { // This callback expires at or after the continuation. We will insert // the continuation *before* this callback. nextAfterContinuation = node; break; } node = node.next; } while (node !== firstCallbackNode); if (nextAfterContinuation === null) { // No equal or lower priority callback was found, which means the new // callback is the lowest priority callback in the list. nextAfterContinuation = firstCallbackNode; } else if (nextAfterContinuation === firstCallbackNode) { // The new callback is the highest priority callback in the list. firstCallbackNode = continuationNode; ensureHostCallbackIsScheduled(); } var previous = nextAfterContinuation.previous; previous.next = nextAfterContinuation.previous = continuationNode; continuationNode.next = nextAfterContinuation; continuationNode.previous = previous; } } }
- 在finally 中执行了
ensureHostCallbackIsScheduled
- 在这个方法中,有个判断是
isHostCallbackScheduled
仅且仅有 在这个判断中,isHostCallbackScheduled 才会被设置为 true - 所以,在有
firstCallbackNode
时,调用 ensureHostCallbackIsScheduled() 时,isHostCallbackScheduled 是 true 的 - 它为 true 时,在 ensureHostCallbackIsScheduled() 中,会执行 cancelHostCallback()
cancelHostCallback = function() { scheduledHostCallback = null; isMessageEventScheduled = false; timeoutTime = -1; };
- 也就是说把之前调度的变量都重置了,不能让老的callback再执行一遍,以此可能导致产生错误
- 在 finally 中的 else 环节 isHostCallbackScheduled 被设置成 false
- 在这个方法中,有个判断是
- 最后执行了
flushImmediateWork
这个API 以后可能会用到function flushImmediateWork() { // 这里 ImmediatePriority 是一个固定的值 但是 firstCallbackNode.priorityLevel // 这里 firstCallbackNode.priorityLevel 是 固定的 3 这个if 不会被执行 // 所以,这个api 暂未开放 if ( // Confirm we've exited the outer most event handler currentEventStartTime === -1 && firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority ) { isExecutingCallback = true; deadlineObject.didTimeout = true; try { do { flushFirstCallback(); } while ( // Keep flushing until there are no more immediate callbacks firstCallbackNode !== null && firstCallbackNode.priorityLevel === ImmediatePriority ); } finally { isExecutingCallback = false; if (firstCallbackNode !== null) { // There's still work remaining. Request another callback. ensureHostCallbackIsScheduled(); } else { isHostCallbackScheduled = false; } } } }
-
- 关于这里的
- 这里最后的
- 第一次计算的
- 判断
-
-
总结
- 以上是 react scheduler 模拟 requestIdleCallback 时间片的操作和调度
- 能够控制把更多的优先权交给浏览器,让它去做动画或用户输入反馈的更新
- 在有空闲的时间,回过头来执行 react 的异步更新操作
- 在这里面会有各种各样的计时来控制帧时间的判断
- 如果发现浏览器的刷新频率更高,则调低帧时间,以及判断任务是否有过期
- 如果过期了,需要强制输出
- 从这个React版本来看,react scheduler 只开放了一部分代码,还有一部分代码暂时没有用到
到了这里,关于React16源码: React中的异步调度scheduler模块的源码实现的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!