mirror of
https://github.com/facebook/react-native.git
synced 2025-11-01 09:14:26 +00:00
Animated: Lazily Allocate AnimatedNode Instances (#46317)
Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/46317 Changes `AnimatedProps` to avoid allocating `AnimatedStyle` (and `AnimatedTransform`, `AnimatedObject`) unless necessary. This not only reduces memory and traversal overhead, but it enables us to implement allowlist strategies to prune unnecessary traversals. Changelog: [General][Changed] - Animated now omits `style` if the supplied value is null, undefined, or not an object. Previously, it would emit an empty `style` object. [General][Changed] - Animated now resolves `style` to the original prop value if it contains no `AnimatedNode` instances. Previously, it would resolve to a flattened style object. Reviewed By: javache Differential Revision: D62117423 fbshipit-source-id: 34b0c9940be5b6f5d94467993a5344406cc56f93
This commit is contained in:
committed by
Facebook GitHub Bot
parent
9e6c4fd342
commit
ca234ba10e
@@ -0,0 +1,31 @@
|
||||
/**
|
||||
* 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.
|
||||
*
|
||||
* @flow strict-local
|
||||
* @format
|
||||
* @oncall react_native
|
||||
*/
|
||||
|
||||
import AnimatedProps from '../nodes/AnimatedProps';
|
||||
|
||||
describe('AnimatedProps', () => {
|
||||
function getValue(inputProps: {[string]: mixed}) {
|
||||
const animatedProps = new AnimatedProps(inputProps, jest.fn());
|
||||
return animatedProps.__getValue();
|
||||
}
|
||||
|
||||
it('returns original `style` if it has no nodes', () => {
|
||||
const style = {color: 'red'};
|
||||
expect(getValue({style}).style).toBe(style);
|
||||
});
|
||||
|
||||
it('returns original `style` for invalid style values', () => {
|
||||
const values = [undefined, null, function () {}, true, 123, 'foo'];
|
||||
for (const value of values) {
|
||||
expect(getValue({style: value})).toEqual({style: value});
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -33,10 +33,14 @@ function createAnimatedProps(
|
||||
const value = inputProps[key];
|
||||
|
||||
if (key === 'style') {
|
||||
const node = new AnimatedStyle(value);
|
||||
nodeKeys.push(key);
|
||||
nodes.push(node);
|
||||
props[key] = node;
|
||||
const node = AnimatedStyle.from(value);
|
||||
if (node == null) {
|
||||
props[key] = value;
|
||||
} else {
|
||||
nodeKeys.push(key);
|
||||
nodes.push(node);
|
||||
props[key] = node;
|
||||
}
|
||||
} else if (value instanceof AnimatedNode) {
|
||||
const node = value;
|
||||
nodeKeys.push(key);
|
||||
|
||||
@@ -38,7 +38,7 @@ function createAnimatedStyle(
|
||||
const node = ReactNativeFeatureFlags.shouldUseAnimatedObjectForTransform()
|
||||
? AnimatedObject.from(value)
|
||||
: // $FlowFixMe[incompatible-call] - `value` is mixed.
|
||||
new AnimatedTransform(value);
|
||||
AnimatedTransform.from(value);
|
||||
if (node == null) {
|
||||
if (keepUnanimatedValues) {
|
||||
style[key] = value;
|
||||
@@ -77,19 +77,36 @@ export default class AnimatedStyle extends AnimatedWithChildren {
|
||||
_inputStyle: any;
|
||||
_style: {[string]: any};
|
||||
|
||||
constructor(inputStyle: any) {
|
||||
super();
|
||||
this._inputStyle = inputStyle;
|
||||
/**
|
||||
* Creates an `AnimatedStyle` if `value` contains `AnimatedNode` instances.
|
||||
* Otherwise, returns `null`.
|
||||
*/
|
||||
static from(inputStyle: any): ?AnimatedStyle {
|
||||
const flatStyle = flattenStyle(inputStyle);
|
||||
if (flatStyle == null) {
|
||||
return null;
|
||||
}
|
||||
const [nodeKeys, nodes, style] = createAnimatedStyle(
|
||||
// NOTE: This null check should not be necessary, but the types are not
|
||||
// strong nor enforced as of this writing. This check should be hoisted
|
||||
// to instantiation sites.
|
||||
flattenStyle(inputStyle) ?? {},
|
||||
flatStyle,
|
||||
Platform.OS !== 'web',
|
||||
);
|
||||
if (nodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
return new AnimatedStyle(nodeKeys, nodes, style, inputStyle);
|
||||
}
|
||||
|
||||
constructor(
|
||||
nodeKeys: $ReadOnlyArray<string>,
|
||||
nodes: $ReadOnlyArray<AnimatedNode>,
|
||||
style: {[string]: any},
|
||||
inputStyle: any,
|
||||
) {
|
||||
super();
|
||||
this.#nodeKeys = nodeKeys;
|
||||
this.#nodes = nodes;
|
||||
this._style = style;
|
||||
this._inputStyle = inputStyle;
|
||||
}
|
||||
|
||||
__getValue(): Object | Array<Object> {
|
||||
|
||||
@@ -26,37 +26,58 @@ type Transform<T = AnimatedNode> = {
|
||||
| {[string]: number | string | T},
|
||||
};
|
||||
|
||||
function flatAnimatedNodes(
|
||||
transforms: $ReadOnlyArray<Transform<>>,
|
||||
): Array<AnimatedNode> {
|
||||
const nodes = [];
|
||||
for (let ii = 0, length = transforms.length; ii < length; ii++) {
|
||||
const transform = transforms[ii];
|
||||
// There should be exactly one property in `transform`.
|
||||
for (const key in transform) {
|
||||
const value = transform[key];
|
||||
if (value instanceof AnimatedNode) {
|
||||
nodes.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
return nodes;
|
||||
}
|
||||
|
||||
export default class AnimatedTransform extends AnimatedWithChildren {
|
||||
// NOTE: For potentially historical reasons, some operations only operate on
|
||||
// the first level of AnimatedNode instances. This optimizes that bevavior.
|
||||
#shallowNodes: $ReadOnlyArray<AnimatedNode>;
|
||||
#nodes: $ReadOnlyArray<AnimatedNode>;
|
||||
|
||||
_transforms: $ReadOnlyArray<Transform<>>;
|
||||
|
||||
constructor(transforms: $ReadOnlyArray<Transform<>>) {
|
||||
super();
|
||||
this._transforms = transforms;
|
||||
|
||||
const shallowNodes = [];
|
||||
// NOTE: This check should not be necessary, but the types are not enforced
|
||||
// as of this writing. This check should be hoisted to instantiation sites.
|
||||
if (Array.isArray(transforms)) {
|
||||
for (let ii = 0, length = transforms.length; ii < length; ii++) {
|
||||
const transform = transforms[ii];
|
||||
// There should be exactly one property in `transform`.
|
||||
for (const key in transform) {
|
||||
const value = transform[key];
|
||||
if (value instanceof AnimatedNode) {
|
||||
shallowNodes.push(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Creates an `AnimatedTransform` if `transforms` contains `AnimatedNode`
|
||||
* instances. Otherwise, returns `null`.
|
||||
*/
|
||||
static from(transforms: $ReadOnlyArray<Transform<>>): ?AnimatedTransform {
|
||||
const nodes = flatAnimatedNodes(
|
||||
// NOTE: This check should not be necessary, but the types are not
|
||||
// enforced as of this writing. This check should be hoisted to
|
||||
// instantiation sites.
|
||||
Array.isArray(transforms) ? transforms : [],
|
||||
);
|
||||
if (nodes.length === 0) {
|
||||
return null;
|
||||
}
|
||||
this.#shallowNodes = shallowNodes;
|
||||
return new AnimatedTransform(nodes, transforms);
|
||||
}
|
||||
|
||||
constructor(
|
||||
nodes: $ReadOnlyArray<AnimatedNode>,
|
||||
transforms: $ReadOnlyArray<Transform<>>,
|
||||
) {
|
||||
super();
|
||||
this.#nodes = nodes;
|
||||
this._transforms = transforms;
|
||||
}
|
||||
|
||||
__makeNative(platformConfig: ?PlatformConfig) {
|
||||
const nodes = this.#shallowNodes;
|
||||
const nodes = this.#nodes;
|
||||
for (let ii = 0, length = nodes.length; ii < length; ii++) {
|
||||
const node = nodes[ii];
|
||||
node.__makeNative(platformConfig);
|
||||
@@ -77,7 +98,7 @@ export default class AnimatedTransform extends AnimatedWithChildren {
|
||||
}
|
||||
|
||||
__attach(): void {
|
||||
const nodes = this.#shallowNodes;
|
||||
const nodes = this.#nodes;
|
||||
for (let ii = 0, length = nodes.length; ii < length; ii++) {
|
||||
const node = nodes[ii];
|
||||
node.__addChild(this);
|
||||
@@ -85,7 +106,7 @@ export default class AnimatedTransform extends AnimatedWithChildren {
|
||||
}
|
||||
|
||||
__detach(): void {
|
||||
const nodes = this.#shallowNodes;
|
||||
const nodes = this.#nodes;
|
||||
for (let ii = 0, length = nodes.length; ii < length; ii++) {
|
||||
const node = nodes[ii];
|
||||
node.__removeChild(this);
|
||||
|
||||
+12
-6
@@ -35,12 +35,18 @@ exports[`LogBoxInspectorSourceMapStatus should render for failed 1`] = `
|
||||
}
|
||||
}
|
||||
style={
|
||||
Object {
|
||||
"height": 14,
|
||||
"marginEnd": 4,
|
||||
"tintColor": "rgba(243, 83, 105, 1)",
|
||||
"width": 16,
|
||||
}
|
||||
Array [
|
||||
Object {
|
||||
"height": 14,
|
||||
"marginEnd": 4,
|
||||
"tintColor": "rgba(255, 255, 255, 0.4)",
|
||||
"width": 16,
|
||||
},
|
||||
Object {
|
||||
"tintColor": "rgba(243, 83, 105, 1)",
|
||||
},
|
||||
null,
|
||||
]
|
||||
}
|
||||
/>
|
||||
<Text
|
||||
|
||||
@@ -970,7 +970,13 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
|
||||
"declare export default class AnimatedStyle extends AnimatedWithChildren {
|
||||
_inputStyle: any;
|
||||
_style: { [string]: any };
|
||||
constructor(inputStyle: any): void;
|
||||
static from(inputStyle: any): ?AnimatedStyle;
|
||||
constructor(
|
||||
nodeKeys: $ReadOnlyArray<string>,
|
||||
nodes: $ReadOnlyArray<AnimatedNode>,
|
||||
style: { [string]: any },
|
||||
inputStyle: any
|
||||
): void;
|
||||
__getValue(): Object | Array<Object>;
|
||||
__getAnimatedValue(): Object;
|
||||
__attach(): void;
|
||||
@@ -1034,7 +1040,11 @@ exports[`public API should not change unintentionally Libraries/Animated/nodes/A
|
||||
};
|
||||
declare export default class AnimatedTransform extends AnimatedWithChildren {
|
||||
_transforms: $ReadOnlyArray<Transform<>>;
|
||||
constructor(transforms: $ReadOnlyArray<Transform<>>): void;
|
||||
static from(transforms: $ReadOnlyArray<Transform<>>): ?AnimatedTransform;
|
||||
constructor(
|
||||
nodes: $ReadOnlyArray<AnimatedNode>,
|
||||
transforms: $ReadOnlyArray<Transform<>>
|
||||
): void;
|
||||
__makeNative(platformConfig: ?PlatformConfig): void;
|
||||
__getValue(): $ReadOnlyArray<Transform<any>>;
|
||||
__getAnimatedValue(): $ReadOnlyArray<Transform<any>>;
|
||||
|
||||
Reference in New Issue
Block a user