Files
react/packages/shared/ReactDOMFrameScheduling.js
2017-12-05 10:39:16 -08:00

222 lines
6.8 KiB
JavaScript

/**
* Copyright (c) 2013-present, Facebook, Inc.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @flow
*/
// This is a built-in 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.
import type {Deadline} from 'react-reconciler';
import ExecutionEnvironment from 'fbjs/lib/ExecutionEnvironment';
import warning from 'fbjs/lib/warning';
if (__DEV__) {
if (
ExecutionEnvironment.canUseDOM &&
typeof requestAnimationFrame !== 'function'
) {
warning(
false,
'React depends on requestAnimationFrame. Make sure that you load a ' +
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
}
const hasNativePerformanceNow =
typeof performance === 'object' && typeof performance.now === 'function';
let now;
if (hasNativePerformanceNow) {
now = function() {
return performance.now();
};
} else {
now = function() {
return Date.now();
};
}
// TODO: There's no way to cancel, because Fiber doesn't atm.
let rIC: (
callback: (deadline: Deadline, options?: {timeout: number}) => void,
) => number;
let cIC: (callbackID: number) => void;
if (!ExecutionEnvironment.canUseDOM) {
rIC = function(
frameCallback: (deadline: Deadline, options?: {timeout: number}) => void,
): number {
return setTimeout(() => {
frameCallback({
timeRemaining() {
return Infinity;
},
});
});
};
cIC = function(timeoutID: number) {
clearTimeout(timeoutID);
};
} else if (
typeof requestIdleCallback !== 'function' ||
typeof cancelIdleCallback !== 'function'
) {
// Polyfill requestIdleCallback and cancelIdleCallback
let scheduledRICCallback = null;
let isIdleScheduled = false;
let timeoutTime = -1;
let isAnimationFrameScheduled = false;
let frameDeadline = 0;
// 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.
let previousFrameTime = 33;
let activeFrameTime = 33;
let frameDeadlineObject;
if (hasNativePerformanceNow) {
frameDeadlineObject = {
didTimeout: false,
timeRemaining() {
// 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.
const remaining = frameDeadline - performance.now();
return remaining > 0 ? remaining : 0;
},
};
} else {
frameDeadlineObject = {
didTimeout: false,
timeRemaining() {
// Fallback to Date.now()
const remaining = frameDeadline - Date.now();
return remaining > 0 ? remaining : 0;
},
};
}
// We use the postMessage trick to defer idle work until after the repaint.
const messageKey =
'__reactIdleCallback$' +
Math.random()
.toString(36)
.slice(2);
const idleTick = function(event) {
if (event.source !== window || event.data !== messageKey) {
return;
}
isIdleScheduled = false;
const currentTime = now();
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 (timeoutTime !== -1 && timeoutTime <= currentTime) {
// Exceeded the timeout. Invoke the callback even though there's no
// time left.
frameDeadlineObject.didTimeout = true;
} else {
// No timeout.
if (!isAnimationFrameScheduled) {
// Schedule another animation callback so we retry later.
isAnimationFrameScheduled = true;
requestAnimationFrame(animationTick);
}
// Exit without invoking the callback.
return;
}
} else {
// There's still time left in this idle period.
frameDeadlineObject.didTimeout = false;
}
timeoutTime = -1;
const callback = scheduledRICCallback;
scheduledRICCallback = null;
if (callback !== null) {
callback(frameDeadlineObject);
}
};
// Assumes that we have addEventListener in this environment. Might need
// something better for old IE.
window.addEventListener('message', idleTick, false);
const animationTick = function(rafTime) {
isAnimationFrameScheduled = false;
let nextFrameTime = rafTime - frameDeadline + activeFrameTime;
if (
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
) {
if (nextFrameTime < 8) {
// Defensive coding. We don't support higher frame rates than 120hz.
// If we get lower than that, 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 (!isIdleScheduled) {
isIdleScheduled = true;
window.postMessage(messageKey, '*');
}
};
rIC = function(
callback: (deadline: Deadline) => void,
options?: {timeout: number},
): number {
// This assumes that we only schedule one callback at a time because that's
// how Fiber uses it.
scheduledRICCallback = callback;
if (options != null && typeof options.timeout === 'number') {
timeoutTime = now() + options.timeout;
}
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;
requestAnimationFrame(animationTick);
}
return 0;
};
cIC = function() {
scheduledRICCallback = null;
isIdleScheduled = false;
timeoutTime = -1;
};
} else {
rIC = window.requestIdleCallback;
cIC = window.cancelIdleCallback;
}
export {now, rIC, cIC};