杭州企业网站搭建,wordpress ssh,仓库进销存管理软件免费版,企业门户网站开发React Scheduler 1 ) 概述
react当中的异步调度#xff0c;称为 React Scheduler发布成单独的一个 npm 包就叫做 scheduler这个包它做了什么#xff1f; A. 首先它维护时间片B. 然后模拟 requestIdleCallback 这个API 因为现在浏览器的支持不是特别的多所以在浏览当中只是去…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 异步的就会调用 scheduleCallbackWithExpirationTimefunction 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. Dont 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.jsexport * 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 是传进来的 performAsyncWorkdeprecated_options 是即将被废弃的 optinos这个即将被废弃 接着处理 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 即将被废弃 如果走到 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 变化了才会去调用因为头部变了 下面为这个方法链表的处理示例 接着进入 ensureHostCallbackIsScheduled 这个方法让队列进入调度的过程function ensureHostCallbackIsScheduled() {if (isExecutingCallback) {// Dont 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) {// Dont wait for the next frame. Continue working ASAP, in a new event.window.postMessage(messageKey, *);} else if (!isAnimationFrameScheduled) {// If rAF didnt already schedule one, we need to schedule a frame.// TODO: If this rAF doesnt 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; 读取 callbacktimeoutTime absoluteTimeout; 是我们传进来的 expirationTime接着判断 if (isFlushingHostCallback || absoluteTimeout 0) 这两种情况不需要等待下一帧去做这个事情而是以最快的速度进入这个方法的调用 window.postMessage(messageKey, *);absoluteTimeout 0 说明已经超时了 不符合上述条件按照正常的调度流程去走 判断 isAnimationFrameScheduled 这个变量的状态如果它没有设置为 true, 则还没有进入调度循环的过程这时候就把它设置为 true, 并执行 requestAnimationFrameWithTimeout(animationTick);进入 requestAnimationFrameWithTimeoutvar requestAnimationFrameWithTimeout function(callback) {// schedule rAF and also a setTimeoutrAFID localRequestAnimationFrame(function(timestamp) {// cancel the setTimeoutlocalClearTimeout(rAFTimeoutID);callback(timestamp);});rAFTimeoutID localSetTimeout(function() {// cancel the requestAnimationFramelocalCancelAnimationFrame(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 its 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 dont 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 thats an indication that we// actually have a higher frame rate than what were currently optimizing.// We adjust our heuristic dynamically accordingly. For example, if were// 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)进入 idleTickvar idleTick function(event) {// 先判断 keyif (event.source ! window || event.data ! messageKey) {return;}isMessageEventScheduled false;// 赋值一份 callbackvar prevScheduledCallback scheduledHostCallback;// 同样处理 timeoutvar prevTimeoutTime timeoutTime;// 重置下面两个scheduledHostCallback null;timeoutTime -1;// 获取当前时间var currentTime getCurrentTime();var didTimeout false;// 这个条件如果 0 说明浏览器动画或用户反馈超过 33ms, 意思是把这一帧的时间已经用完了// 对于 react 来说它已经没有时间执行它的更新了if (frameDeadline - currentTime 0) {// Theres no time left in this idle period. Check if the callback has// a timeout and whether its been exceeded.// 进入上述条件它需要继续判断 timeout 是否已经过期或者小于当前时间(说明任务也已经过期了)// 如果任务已经过期这个任务就需要强行被更新// 可以在任务没有过期的时候判断帧时间如果没有了即: frameDeadline - currentTime 0// 先跳过等下一帧来更新但是在任务已经过期的时候就需要强制执行了于是就设置了下面的 didTimeout true;if (prevTimeoutTime ! -1 prevTimeoutTime currentTime) {// Exceeded the timeout. Invoke the callback even though theres 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 并调用 prevScheduledCallbackif (prevScheduledCallback ! null) {isFlushingHostCallback true;try {prevScheduledCallback(didTimeout);} finally {isFlushingHostCallback false;}}
};这里最后的 prevScheduledCallback 向上溯源找到 ensureHostCallbackIsScheduled function ensureHostCallbackIsScheduled() {if (isExecutingCallback) {// Dont 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) {// Theres 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 whileflushFirstCallback 是真正调用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;// 说明链表里只有一个节点直接设置 nullif (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 its 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 就是传进来的 performAsyncWorkcontinuationCallback 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 weve exited the outer most event handlercurrentEventStartTime -1 firstCallbackNode ! null firstCallbackNode.priorityLevel ImmediatePriority) {isExecutingCallback true;deadlineObject.didTimeout true;try {do {flushFirstCallback();} while (// Keep flushing until there are no more immediate callbacksfirstCallbackNode ! null firstCallbackNode.priorityLevel ImmediatePriority);} finally {isExecutingCallback false;if (firstCallbackNode ! null) {// Theres still work remaining. Request another callback.ensureHostCallbackIsScheduled();} else {isHostCallbackScheduled false;}}}
}总结
以上是 react scheduler 模拟 requestIdleCallback 时间片的操作和调度能够控制把更多的优先权交给浏览器让它去做动画或用户输入反馈的更新在有空闲的时间回过头来执行 react 的异步更新操作在这里面会有各种各样的计时来控制帧时间的判断如果发现浏览器的刷新频率更高则调低帧时间以及判断任务是否有过期如果过期了需要强制输出从这个React版本来看react scheduler 只开放了一部分代码还有一部分代码暂时没有用到