react-native
Version:
A framework for building native apps using React
488 lines (431 loc) • 13.8 kB
JavaScript
/**
* Copyright (c) Meta Platforms, Inc. and affiliates.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
* @format
* @flow
*/
import NativeTiming from './NativeTiming';
const BatchedBridge = require('../../BatchedBridge/BatchedBridge');
const Systrace = require('../../Performance/Systrace');
const invariant = require('invariant');
/**
* JS implementation of timer functions. Must be completely driven by an
* external clock signal, all that's stored here is timerID, timer type, and
* callback.
*/
export type JSTimerType =
| 'setTimeout'
| 'setInterval'
| 'requestAnimationFrame'
| 'queueReactNativeMicrotask'
| 'requestIdleCallback';
// These timing constants should be kept in sync with the ones in native ios and
// android `RCTTiming` module.
const FRAME_DURATION = 1000 / 60;
const IDLE_CALLBACK_FRAME_DEADLINE = 1;
// Parallel arrays
const callbacks: Array<?Function> = [];
const types: Array<?JSTimerType> = [];
const timerIDs: Array<?number> = [];
let reactNativeMicrotasks: Array<number> = [];
let requestIdleCallbacks: Array<number> = [];
const requestIdleCallbackTimeouts: {[number]: number, ...} = {};
let GUID = 1;
const errors: Array<Error> = [];
let hasEmittedTimeDriftWarning = false;
// Returns a free index if one is available, and the next consecutive index otherwise.
function _getFreeIndex(): number {
let freeIndex = timerIDs.indexOf(null);
if (freeIndex === -1) {
freeIndex = timerIDs.length;
}
return freeIndex;
}
function _allocateCallback(func: Function, type: JSTimerType): number {
const id = GUID++;
const freeIndex = _getFreeIndex();
timerIDs[freeIndex] = id;
callbacks[freeIndex] = func;
types[freeIndex] = type;
return id;
}
/**
* Calls the callback associated with the ID. Also unregister that callback
* if it was a one time timer (setTimeout), and not unregister it if it was
* recurring (setInterval).
*/
function _callTimer(timerID: number, frameTime: number, didTimeout: ?boolean) {
if (timerID > GUID) {
console.warn(
'Tried to call timer with ID %s but no such timer exists.',
timerID,
);
}
// timerIndex of -1 means that no timer with that ID exists. There are
// two situations when this happens, when a garbage timer ID was given
// and when a previously existing timer was deleted before this callback
// fired. In both cases we want to ignore the timer id, but in the former
// case we warn as well.
const timerIndex = timerIDs.indexOf(timerID);
if (timerIndex === -1) {
return;
}
const type = types[timerIndex];
const callback = callbacks[timerIndex];
if (!callback || !type) {
console.error('No callback found for timerID ' + timerID);
return;
}
if (__DEV__) {
Systrace.beginEvent(type + ' [invoke]');
}
// Clear the metadata
if (type !== 'setInterval') {
_clearIndex(timerIndex);
}
try {
if (
type === 'setTimeout' ||
type === 'setInterval' ||
type === 'queueReactNativeMicrotask'
) {
callback();
} else if (type === 'requestAnimationFrame') {
callback(global.performance.now());
} else if (type === 'requestIdleCallback') {
callback({
timeRemaining: function () {
// TODO: Optimisation: allow running for longer than one frame if
// there are no pending JS calls on the bridge from native. This
// would require a way to check the bridge queue synchronously.
return Math.max(
0,
FRAME_DURATION - (global.performance.now() - frameTime),
);
},
didTimeout: !!didTimeout,
});
} else {
console.error('Tried to call a callback with invalid type: ' + type);
}
} catch (e) {
// Don't rethrow so that we can run all timers.
errors.push(e);
}
if (__DEV__) {
Systrace.endEvent();
}
}
/**
* Performs a single pass over the enqueued reactNativeMicrotasks. Returns whether
* more reactNativeMicrotasks are queued up (can be used as a condition a while loop).
*/
function _callReactNativeMicrotasksPass() {
if (reactNativeMicrotasks.length === 0) {
return false;
}
if (__DEV__) {
Systrace.beginEvent('callReactNativeMicrotasksPass()');
}
// The main reason to extract a single pass is so that we can track
// in the system trace
const passReactNativeMicrotasks = reactNativeMicrotasks;
reactNativeMicrotasks = [];
// Use for loop rather than forEach as per @vjeux's advice
// https://github.com/facebook/react-native/commit/c8fd9f7588ad02d2293cac7224715f4af7b0f352#commitcomment-14570051
for (let i = 0; i < passReactNativeMicrotasks.length; ++i) {
_callTimer(passReactNativeMicrotasks[i], 0);
}
if (__DEV__) {
Systrace.endEvent();
}
return reactNativeMicrotasks.length > 0;
}
function _clearIndex(i: number) {
timerIDs[i] = null;
callbacks[i] = null;
types[i] = null;
}
function _freeCallback(timerID: number) {
// timerIDs contains nulls after timers have been removed;
// ignore nulls upfront so indexOf doesn't find them
if (timerID == null) {
return;
}
const index = timerIDs.indexOf(timerID);
// See corresponding comment in `callTimers` for reasoning behind this
if (index !== -1) {
const type = types[index];
_clearIndex(index);
if (
type !== 'queueReactNativeMicrotask' &&
type !== 'requestIdleCallback'
) {
deleteTimer(timerID);
}
}
}
/**
* JS implementation of timer functions. Must be completely driven by an
* external clock signal, all that's stored here is timerID, timer type, and
* callback.
*/
const JSTimers = {
/**
* @param {function} func Callback to be invoked after `duration` ms.
* @param {number} duration Number of milliseconds.
*/
setTimeout: function (
func: Function,
duration: number,
...args: any
): number {
const id = _allocateCallback(
() => func.apply(undefined, args),
'setTimeout',
);
createTimer(id, duration || 0, Date.now(), /* recurring */ false);
return id;
},
/**
* @param {function} func Callback to be invoked every `duration` ms.
* @param {number} duration Number of milliseconds.
*/
setInterval: function (
func: Function,
duration: number,
...args: any
): number {
const id = _allocateCallback(
() => func.apply(undefined, args),
'setInterval',
);
createTimer(id, duration || 0, Date.now(), /* recurring */ true);
return id;
},
/**
* The React Native microtask mechanism is used to back public APIs e.g.
* `queueMicrotask`, `clearImmediate`, and `setImmediate` (which is used by
* the Promise polyfill) when the JSVM microtask mechanism is not used.
*
* @param {function} func Callback to be invoked before the end of the
* current JavaScript execution loop.
*/
queueReactNativeMicrotask: function (func: Function, ...args: any): number {
const id = _allocateCallback(
() => func.apply(undefined, args),
'queueReactNativeMicrotask',
);
reactNativeMicrotasks.push(id);
return id;
},
/**
* @param {function} func Callback to be invoked every frame.
*/
requestAnimationFrame: function (func: Function): any | number {
const id = _allocateCallback(func, 'requestAnimationFrame');
createTimer(id, 1, Date.now(), /* recurring */ false);
return id;
},
/**
* @param {function} func Callback to be invoked every frame and provided
* with time remaining in frame.
* @param {?object} options
*/
requestIdleCallback: function (
func: Function,
options: ?Object,
): any | number {
if (requestIdleCallbacks.length === 0) {
setSendIdleEvents(true);
}
const timeout = options && options.timeout;
const id: number = _allocateCallback(
timeout != null
? (deadline: any) => {
const timeoutId: number = requestIdleCallbackTimeouts[id];
if (timeoutId) {
JSTimers.clearTimeout(timeoutId);
delete requestIdleCallbackTimeouts[id];
}
return func(deadline);
}
: func,
'requestIdleCallback',
);
requestIdleCallbacks.push(id);
if (timeout != null) {
const timeoutId: number = JSTimers.setTimeout(() => {
const index: number = requestIdleCallbacks.indexOf(id);
if (index > -1) {
requestIdleCallbacks.splice(index, 1);
_callTimer(id, global.performance.now(), true);
}
delete requestIdleCallbackTimeouts[id];
if (requestIdleCallbacks.length === 0) {
setSendIdleEvents(false);
}
}, timeout);
requestIdleCallbackTimeouts[id] = timeoutId;
}
return id;
},
cancelIdleCallback: function (timerID: number) {
_freeCallback(timerID);
const index = requestIdleCallbacks.indexOf(timerID);
if (index !== -1) {
requestIdleCallbacks.splice(index, 1);
}
const timeoutId = requestIdleCallbackTimeouts[timerID];
if (timeoutId) {
JSTimers.clearTimeout(timeoutId);
delete requestIdleCallbackTimeouts[timerID];
}
if (requestIdleCallbacks.length === 0) {
setSendIdleEvents(false);
}
},
clearTimeout: function (timerID: number) {
_freeCallback(timerID);
},
clearInterval: function (timerID: number) {
_freeCallback(timerID);
},
clearReactNativeMicrotask: function (timerID: number) {
_freeCallback(timerID);
const index = reactNativeMicrotasks.indexOf(timerID);
if (index !== -1) {
reactNativeMicrotasks.splice(index, 1);
}
},
cancelAnimationFrame: function (timerID: number) {
_freeCallback(timerID);
},
/**
* This is called from the native side. We are passed an array of timerIDs,
* and
*/
callTimers: function (timersToCall: Array<number>): any | void {
invariant(
timersToCall.length !== 0,
'Cannot call `callTimers` with an empty list of IDs.',
);
errors.length = 0;
for (let i = 0; i < timersToCall.length; i++) {
_callTimer(timersToCall[i], 0);
}
const errorCount = errors.length;
if (errorCount > 0) {
if (errorCount > 1) {
// Throw all the other errors in a setTimeout, which will throw each
// error one at a time
for (let ii = 1; ii < errorCount; ii++) {
JSTimers.setTimeout(
((error: Error) => {
throw error;
}).bind(null, errors[ii]),
0,
);
}
}
throw errors[0];
}
},
callIdleCallbacks: function (frameTime: number) {
if (
FRAME_DURATION - (Date.now() - frameTime) <
IDLE_CALLBACK_FRAME_DEADLINE
) {
return;
}
errors.length = 0;
if (requestIdleCallbacks.length > 0) {
const passIdleCallbacks = requestIdleCallbacks;
requestIdleCallbacks = [];
for (let i = 0; i < passIdleCallbacks.length; ++i) {
_callTimer(passIdleCallbacks[i], frameTime);
}
}
if (requestIdleCallbacks.length === 0) {
setSendIdleEvents(false);
}
errors.forEach(error =>
JSTimers.setTimeout(() => {
throw error;
}, 0),
);
},
/**
* This is called after we execute any command we receive from native but
* before we hand control back to native.
*/
callReactNativeMicrotasks() {
errors.length = 0;
while (_callReactNativeMicrotasksPass()) {}
errors.forEach(error =>
JSTimers.setTimeout(() => {
throw error;
}, 0),
);
},
/**
* Called from native (in development) when environment times are out-of-sync.
*/
emitTimeDriftWarning(warningMessage: string) {
if (hasEmittedTimeDriftWarning) {
return;
}
hasEmittedTimeDriftWarning = true;
console.warn(warningMessage);
},
};
function createTimer(
callbackID: number,
duration: number,
jsSchedulingTime: number,
repeats: boolean,
): void {
invariant(NativeTiming, 'NativeTiming is available');
NativeTiming.createTimer(callbackID, duration, jsSchedulingTime, repeats);
}
function deleteTimer(timerID: number): void {
invariant(NativeTiming, 'NativeTiming is available');
NativeTiming.deleteTimer(timerID);
}
function setSendIdleEvents(sendIdleEvents: boolean): void {
invariant(NativeTiming, 'NativeTiming is available');
NativeTiming.setSendIdleEvents(sendIdleEvents);
}
let ExportedJSTimers: {|
callIdleCallbacks: (frameTime: number) => any | void,
callReactNativeMicrotasks: () => void,
callTimers: (timersToCall: Array<number>) => any | void,
cancelAnimationFrame: (timerID: number) => void,
cancelIdleCallback: (timerID: number) => void,
clearReactNativeMicrotask: (timerID: number) => void,
clearInterval: (timerID: number) => void,
clearTimeout: (timerID: number) => void,
emitTimeDriftWarning: (warningMessage: string) => any | void,
requestAnimationFrame: (func: any) => any | number,
requestIdleCallback: (func: any, options: ?any) => any | number,
queueReactNativeMicrotask: (func: any, ...args: any) => number,
setInterval: (func: any, duration: number, ...args: any) => number,
setTimeout: (func: any, duration: number, ...args: any) => number,
|};
if (!NativeTiming) {
console.warn("Timing native module is not available, can't set timers.");
// $FlowFixMe[prop-missing] : we can assume timers are generally available
ExportedJSTimers = ({
callReactNativeMicrotasks: JSTimers.callReactNativeMicrotasks,
queueReactNativeMicrotask: JSTimers.queueReactNativeMicrotask,
}: typeof JSTimers);
} else {
ExportedJSTimers = JSTimers;
}
BatchedBridge.setReactNativeMicrotasksCallback(
JSTimers.callReactNativeMicrotasks,
);
module.exports = ExportedJSTimers;