react/packages/shared/ReactDOMFrameScheduling.js

225 lines
7.0 KiB
JavaScript
Raw Normal View History

/**
* 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.
Reorganize code structure (#11288) * Move files and tests to more meaningful places * Fix the build Now that we import reconciler via react-reconciler, I needed to make a few tweaks. * Update sizes * Move @preventMunge directive to FB header * Revert unintentional change * Fix Flow coverage I forgot to @flow-ify those files. This uncovered some issues. * Prettier, I love you but you're bringing me down Prettier, I love you but you're bringing me down Like a rat in a cage Pulling minimum wage Prettier, I love you but you're bringing me down Prettier, you're safer and you're wasting my time Our records all show you were filthy but fine But they shuttered your stores When you opened the doors To the cops who were bored once they'd run out of crime Prettier, you're perfect, oh, please don't change a thing Your mild billionaire mayor's now convinced he's a king So the boring collect I mean all disrespect In the neighborhood bars I'd once dreamt I would drink Prettier, I love you but you're freaking me out There's a ton of the twist but we're fresh out of shout Like a death in the hall That you hear through your wall Prettier, I love you but you're freaking me out Prettier, I love you but you're bringing me down Prettier, I love you but you're bringing me down Like a death of the heart Jesus, where do I start? But you're still the one pool where I'd happily drown And oh! Take me off your mailing list For kids who think it still exists Yes, for those who think it still exists Maybe I'm wrong and maybe you're right Maybe I'm wrong and maybe you're right Maybe you're right, maybe I'm wrong And just maybe you're right And oh! Maybe mother told you true And there'll always be somebody there for you And you'll never be alone But maybe she's wrong and maybe I'm right And just maybe she's wrong Maybe she's wrong and maybe I'm right And if so, here's this song!
2017-10-20 02:50:24 +08:00
import type {Deadline} from 'react-reconciler';
import {alwaysUseRequestIdleCallbackPolyfill} from 'shared/ReactFeatureFlags';
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 ' +
2017-12-06 02:39:16 +08:00
'polyfill in older browsers. https://fb.me/react-polyfills',
);
}
}
Expiration times (#10426) * [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions
2017-10-10 09:39:53 +08:00
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;
},
didTimeout: false,
});
});
};
cIC = function(timeoutID: number) {
clearTimeout(timeoutID);
};
} else if (
alwaysUseRequestIdleCallbackPolyfill ||
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;
Expiration times (#10426) * [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions
2017-10-10 09:39:53 +08:00
if (hasNativePerformanceNow) {
frameDeadlineObject = {
didTimeout: false,
Expiration times (#10426) * [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions
2017-10-10 09:39:53 +08:00
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;
Expiration times (#10426) * [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions
2017-10-10 09:39:53 +08:00
},
};
} else {
frameDeadlineObject = {
didTimeout: false,
Expiration times (#10426) * [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions
2017-10-10 09:39:53 +08:00
timeRemaining() {
// Fallback to Date.now()
const remaining = frameDeadline - Date.now();
return remaining > 0 ? remaining : 0;
Expiration times (#10426) * [Work-in-progress] Assign expiration times to updates An expiration time represents a time in the future by which an update should flush. The priority of the update is related to the difference between the current clock time and the expiration time. This has the effect of increasing the priority of updates as time progresses, to prevent starvation. This lays the initial groundwork for expiration times without changing any behavior. Future commits will replace work priority with expiration times. * Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. * Triangle Demo should use a class shouldComponentUpdate was removed from functional components. Running the demo shows, now that expiration is enabled, the demo does not starve. (Still won't run smoothly until we add back the ability to resume interrupted work.) * Use a magic value for task expiration time There are a few cases related to sync mode where we need to distinguish between work that is scheduled as task and work that is treated like task because it expires. For example, batchedUpdates. We don't want to perform any work until the end of the batch, regardless of how much time has elapsed. * Use current time to calculate expiration time * Add unit tests for expiration and coalescing * Delete unnecessary abstraction * Move performance.now polyfill to ReactDOMFrameScheduling * Add expiration to fuzz tester * Expiration nits - Rename Done -> NoWork - Use max int32 instead of max safe int - Use bitwise operations instead of Math functions
2017-10-10 09:39:53 +08:00
},
};
}
// 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;
2017-03-14 08:05:18 +08:00
if (
2017-04-21 02:18:33 +08:00
nextFrameTime < activeFrameTime &&
previousFrameTime < activeFrameTime
2017-03-14 08:05:18 +08:00
) {
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};