// The remaining code is essentially a polyfill for requestIdleCallback. It // works by scheduling a requestAnimationFrame, storing the time for the start // of the frame, then scheduling a postMessage which gets scheduled after paint. // Within the postMessage handler do as much work as possible until time + frame // rate. By separating the idle call into a separate event tick we ensure that // layout, paint and other browser work is counted against the available time. // The frame rate is dynamically adjusted.
// We capture a local reference to any global, in case it gets polyfilled after // this module is initially evaluated. We want to be using a // consistent implementation.
// This initialization code may run even on server environments if a component // just imports ReactDOM (e.g. for findDOMNode). Some environments might not // have setTimeout or clearTimeout. However, we always expect them to be defined // on the client. https://github.com/facebook/react/pull/13088 var localSetTimeout = typeofsetTimeout === 'function' ? setTimeout : undefined; var localClearTimeout = typeofclearTimeout === 'function' ? clearTimeout : undefined;
// We don't expect either of these to necessarily be defined, but we will error // later if they are missing on the client. var localRequestAnimationFrame = typeof requestAnimationFrame === 'function' ? requestAnimationFrame : undefined; var localCancelAnimationFrame = typeof cancelAnimationFrame === 'function' ? cancelAnimationFrame : undefined;
var getCurrentTime;
// requestAnimationFrame does not run when the tab is in the background. If // we're backgrounded we prefer for that work to happen so that the page // continues to load in the background. So we also schedule a 'setTimeout' as // a fallback. // TODO: Need a better heuristic for backgrounded work. varANIMATION_FRAME_TIMEOUT = 100; var rAFID; var rAFTimeoutID; 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()); }, ANIMATION_FRAME_TIMEOUT); };
if (typeofconsole !== 'undefined') { // TODO: Remove fb.me link if (typeof localRequestAnimationFrame !== 'function') { console.error( "This browser doesn't support requestAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills', ); } if (typeof localCancelAnimationFrame !== 'function') { console.error( "This browser doesn't support cancelAnimationFrame. " + 'Make sure that you load a ' + 'polyfill in older browsers. https://fb.me/react-polyfills', ); } }
然后是几个变量,理解这几个变量的作用对于理解接下来的代码比较重要:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
var scheduledHostCallback = null; // 当前正在执行的任务,可以类比为上面的_callback var isMessageEventScheduled = false; // 是否有正在处理的MessageChannel消息 var timeoutTime = -1; // 当然任务的超时时间
var isAnimationFrameScheduled = false; // 是否有任务被requestAnimationFrame加入宏任务
var isFlushingHostCallback = false; // 是否有任务正在执行
var frameDeadline = 0; // 记录当前帧的到期时间,他等于rafTime + activeFrameTime,也就是requestAnimationFrame回调传入的时间,加上一帧的时间
// We start out assuming that we run at 30fps but then the heuristic tracking // will adjust this value to a faster fps if we get more frequent animation // frames. var previousFrameTime = 33; var activeFrameTime = 33;
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. requestAnimationFrameWithTimeout(animationTick); } else { // No pending work. Exit. isAnimationFrameScheduled = false; return; }
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; port.postMessage(undefined); } };
var prevScheduledCallback = scheduledHostCallback; var prevTimeoutTime = timeoutTime; scheduledHostCallback = null; timeoutTime = -1;
var currentTime = getCurrentTime();
var didTimeout = false; 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. if (prevTimeoutTime !== -1 && prevTimeoutTime <= currentTime) { // Exceeded the timeout. Invoke the callback even though there's no // time left. didTimeout = true; } else { // No timeout. if (!isAnimationFrameScheduled) { // Schedule another animation callback so we retry later. isAnimationFrameScheduled = true; requestAnimationFrameWithTimeout(animationTick); } // Exit without invoking the callback. scheduledHostCallback = prevScheduledCallback; timeoutTime = prevTimeoutTime; return; } }
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. port.postMessage(undefined); } elseif (!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); } };
// Max 31 bit integer. The max integer size in V8 for 32-bit systems. // Math.pow(2, 30) - 1 // 0b111111111111111111111111111111 var maxSigned31BitInt = 1073741823;
// Times out immediately varIMMEDIATE_PRIORITY_TIMEOUT = -1; // Eventually times out varUSER_BLOCKING_PRIORITY = 250; varNORMAL_PRIORITY_TIMEOUT = 5000; varLOW_PRIORITY_TIMEOUT = 10000; // Never times out varIDLE_PRIORITY = maxSigned31BitInt;
// Callbacks are stored as a circular, doubly linked list. var firstCallbackNode = null;
var currentDidTimeout = false; // Pausing the scheduler is useful for debugging. var isSchedulerPaused = false;
var currentPriorityLevel = NormalPriority; var currentEventStartTime = -1; var currentExpirationTime = -1;
// This is set when a callback is being executed, to prevent re-entrancy. var isExecutingCallback = false;
functionflushFirstCallback() { // 保存firstCallbackNode为flushedNode等会使用 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; if (firstCallbackNode === next) { // This is the last callback in the list. firstCallbackNode = null; next = null; } else { // 通过让firstCallbackNode的previous指向firstCallbackNode的next来将firstCallbackNode从任务环中删除 // 同时让firstCallbackNode指向下一个节点 var lastCallbackNode = firstCallbackNode.previous; firstCallbackNode = lastCallbackNode.next = next; next.previous = lastCallbackNode; }
// Now it's safe to call the callback. // 执行当前节点的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 { continuationCallback = callback(); } 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. if (typeof continuationCallback === 'function') { varcontinuationNode: 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; } elseif (nextAfterContinuation === firstCallbackNode) { // The new callback is the highest priority callback in the list. firstCallbackNode = continuationNode; ensureHostCallbackIsScheduled(); }
functionensureHostCallbackIsScheduled() { 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); }
functionflushWork(didTimeout) { // Exit right away if we're currently paused
if (enableSchedulerDebugging && isSchedulerPaused) { return; }
isExecutingCallback = true; const previousDidTimeout = currentDidTimeout; currentDidTimeout = didTimeout; try { if (didTimeout) { // Flush all the expired callbacks without yielding. while ( firstCallbackNode !== null && !(enableSchedulerDebugging && isSchedulerPaused) ) { // TODO Wrap in feature flag // 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) { do { flushFirstCallback(); } while ( firstCallbackNode !== null && firstCallbackNode.expirationTime <= currentTime && !(enableSchedulerDebugging && isSchedulerPaused) ); continue; } break; } } else { // Keep flushing callbacks until we run out of time in the frame. if (firstCallbackNode !== null) { do { if (enableSchedulerDebugging && isSchedulerPaused) { break; } flushFirstCallback(); } while (firstCallbackNode !== null && !shouldYieldToHost()); } } } finally { isExecutingCallback = false; currentDidTimeout = previousDidTimeout; 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(); } }
// 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; } elseif (next === firstCallbackNode) { // The new callback has the earliest expiration in the entire list. firstCallbackNode = newNode; ensureHostCallbackIsScheduled(); }
functionunstable_cancelCallback(callbackNode) { var next = callbackNode.next; if (next === null) { // Already cancelled. return; }
if (next === callbackNode) { // This is the only scheduled callback. Clear the list. firstCallbackNode = null; } else { // Remove the callback from its position in the list. if (callbackNode === firstCallbackNode) { firstCallbackNode = next; } var previous = callbackNode.previous; previous.next = next; next.previous = previous; }