Skip to content

Commit ac5b754

Browse files
sebmarkbagefacebook-github-bot-4
authored andcommitted
Refactor Attribute Processing (Step 3)
Summary: Decouple processStyle from the main reconciliation. It is now a process extension to the style attribute `transform`. This effectively decouples a large portion of special cases and helper dependencies from the reconciler. The transform attribute becomes translated into the transformMatrix attribute on the native side so this becomes a little weird in that I have to special case it. I don't think it is worth while having a general solution for this so I intend to rename the native attribute to `transform` and just have it accept the resolved transform. Then I can remove the special cases. The next step is generalizing the flattenStyle function and optimizing it. @​public Reviewed By: @vjeux Differential Revision: D2460465 fb-gh-sync-id: 243e7fd77d282b401bc2c028aec8d57f24522a8e
1 parent 8e3ce0f commit ac5b754

6 files changed

Lines changed: 148 additions & 91 deletions

File tree

Libraries/Components/View/ReactNativeStyleAttributes.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var ViewStylePropTypes = require('ViewStylePropTypes');
1919
var keyMirror = require('keyMirror');
2020
var matricesDiffer = require('matricesDiffer');
2121
var processColor = require('processColor');
22+
var processTransform = require('processTransform');
2223
var sizesDiffer = require('sizesDiffer');
2324

2425
var ReactNativeStyleAttributes = {
@@ -27,6 +28,7 @@ var ReactNativeStyleAttributes = {
2728
...keyMirror(ImageStylePropTypes),
2829
};
2930

31+
ReactNativeStyleAttributes.transform = { process: processTransform };
3032
ReactNativeStyleAttributes.transformMatrix = { diff: matricesDiffer };
3133
ReactNativeStyleAttributes.shadowOffset = { diff: sizesDiffer };
3234

Libraries/ReactIOS/NativeMethodsMixin.js

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,10 +17,7 @@ var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
1717
var TextInputState = require('TextInputState');
1818

1919
var findNodeHandle = require('findNodeHandle');
20-
var flattenStyle = require('flattenStyle');
2120
var invariant = require('invariant');
22-
var mergeFast = require('mergeFast');
23-
var precomputeStyle = require('precomputeStyle');
2421

2522
type MeasureOnSuccessCallback = (
2623
x: number,

Libraries/ReactNative/ReactNativeAttributePayload.js

Lines changed: 32 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@
1111
*/
1212
'use strict';
1313

14+
var Platform = require('Platform');
15+
1416
var deepDiffer = require('deepDiffer');
1517
var styleDiffer = require('styleDiffer');
1618
var flattenStyle = require('flattenStyle');
17-
var precomputeStyle = require('precomputeStyle');
1819

1920
type AttributeDiffer = (prevProp : mixed, nextProp : mixed) => boolean;
2021
type AttributePreprocessor = (nextProp: mixed) => mixed;
@@ -29,6 +30,21 @@ type AttributeConfiguration =
2930
CustomAttributeConfiguration | AttributeConfiguration /*| boolean*/
3031
) };
3132

33+
function translateKey(propKey : string) : string {
34+
if (propKey === 'transform') {
35+
// We currently special case the key for `transform`. iOS uses the
36+
// transformMatrix name and Android uses the decomposedMatrix name.
37+
// TODO: We could unify these names and just use the name `transform`
38+
// all the time. Just need to update the native side.
39+
if (Platform.OS === 'android') {
40+
return 'decomposedMatrix';
41+
} else {
42+
return 'transformMatrix';
43+
}
44+
}
45+
return propKey;
46+
}
47+
3248
function defaultDiffer(prevProp: mixed, nextProp: mixed) : boolean {
3349
if (typeof nextProp !== 'object' || nextProp === null) {
3450
// Scalars have already been checked for equality
@@ -55,17 +71,9 @@ function diffNestedProperty(
5571
}
5672

5773
// TODO: Walk both props in parallel instead of flattening.
58-
// TODO: Move precomputeStyle to .process for each attribute.
59-
60-
var previousFlattenedStyle = precomputeStyle(
61-
flattenStyle(prevProp),
62-
validAttributes
63-
);
6474

65-
var nextFlattenedStyle = precomputeStyle(
66-
flattenStyle(nextProp),
67-
validAttributes
68-
);
75+
var previousFlattenedStyle = flattenStyle(prevProp);
76+
var nextFlattenedStyle = flattenStyle(nextProp);
6977

7078
if (!previousFlattenedStyle || !nextFlattenedStyle) {
7179
if (nextFlattenedStyle) {
@@ -144,7 +152,14 @@ function diffProperties(
144152
if (!attributeConfig) {
145153
continue; // not a valid native prop
146154
}
147-
if (updatePayload && updatePayload[propKey] !== undefined) {
155+
156+
var altKey = translateKey(propKey);
157+
if (!attributeConfig[altKey]) {
158+
// If there is no config for the alternative, bail out. Helps ART.
159+
altKey = propKey;
160+
}
161+
162+
if (updatePayload && updatePayload[altKey] !== undefined) {
148163
// If we're in a nested attribute set, we may have set this property
149164
// already. If so, bail out. The previous update is what counts.
150165
continue;
@@ -172,7 +187,7 @@ function diffProperties(
172187
// case: !Object is the default case
173188
if (defaultDiffer(prevProp, nextProp)) {
174189
// a normal leaf has changed
175-
(updatePayload || (updatePayload = {}))[propKey] = nextProp;
190+
(updatePayload || (updatePayload = {}))[altKey] = nextProp;
176191
}
177192
} else if (typeof attributeConfig.diff === 'function' ||
178193
typeof attributeConfig.process === 'function') {
@@ -186,7 +201,7 @@ function diffProperties(
186201
var nextValue = typeof attributeConfig.process === 'function' ?
187202
attributeConfig.process(nextProp) :
188203
nextProp;
189-
(updatePayload || (updatePayload = {}))[propKey] = nextValue;
204+
(updatePayload || (updatePayload = {}))[altKey] = nextValue;
190205
}
191206
} else {
192207
// default: fallthrough case when nested properties are defined
@@ -210,6 +225,7 @@ function diffProperties(
210225
if (!attributeConfig) {
211226
continue; // not a valid native prop
212227
}
228+
213229
prevProp = prevProps[propKey];
214230
if (prevProp === undefined) {
215231
continue; // was already empty anyway
@@ -218,9 +234,10 @@ function diffProperties(
218234
if (typeof attributeConfig !== 'object' ||
219235
typeof attributeConfig.diff === 'function' ||
220236
typeof attributeConfig.process === 'function') {
237+
221238
// case: CustomAttributeConfiguration | !Object
222239
// Flag the leaf property for removal by sending a sentinel.
223-
(updatePayload || (updatePayload = {}))[propKey] = null;
240+
(updatePayload || (updatePayload = {}))[translateKey(propKey)] = null;
224241
} else {
225242
// default:
226243
// This is a nested attribute configuration where all the properties

Libraries/ReactNative/__tests__/ReactNativeAttributePayload-test.js

Lines changed: 92 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,13 @@
44
'use strict';
55

66
jest.dontMock('ReactNativeAttributePayload');
7+
jest.dontMock('StyleSheetRegistry');
78
jest.dontMock('deepDiffer');
8-
jest.dontMock('styleDiffer');
9-
jest.dontMock('precomputeStyle');
109
jest.dontMock('flattenStyle');
10+
jest.dontMock('styleDiffer');
11+
1112
var ReactNativeAttributePayload = require('ReactNativeAttributePayload');
13+
var StyleSheetRegistry = require('StyleSheetRegistry');
1214

1315
var diff = ReactNativeAttributePayload.diff;
1416

@@ -122,6 +124,94 @@ describe('ReactNativeAttributePayload', function() {
122124
)).toEqual(null);
123125
});
124126

127+
it('should flatten nested styles and predefined styles', () => {
128+
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
129+
130+
expect(diff(
131+
{},
132+
{ someStyle: [{ foo: 1 }, { bar: 2 }]},
133+
validStyleAttribute
134+
)).toEqual({ foo: 1, bar: 2 });
135+
136+
expect(diff(
137+
{ someStyle: [{ foo: 1 }, { bar: 2 }]},
138+
{},
139+
validStyleAttribute
140+
)).toEqual({ foo: null, bar: null });
141+
142+
var barStyle = StyleSheetRegistry.registerStyle({
143+
bar: 3,
144+
});
145+
146+
expect(diff(
147+
{},
148+
{ someStyle: [[{ foo: 1 }, { foo: 2 }], barStyle]},
149+
validStyleAttribute
150+
)).toEqual({ foo: 2, bar: 3 });
151+
});
152+
153+
it('should reset a value to a previous if it is removed', () => {
154+
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
155+
156+
expect(diff(
157+
{ someStyle: [{ foo: 1 }, { foo: 3 }]},
158+
{ someStyle: [{ foo: 1 }, { bar: 2 }]},
159+
validStyleAttribute
160+
)).toEqual({ foo: 1, bar: 2 });
161+
});
162+
163+
it('should not clear removed props if they are still in another slot', () => {
164+
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
165+
166+
expect(diff(
167+
{ someStyle: [{}, { foo: 3, bar: 2 }]},
168+
{ someStyle: [{ foo: 3 }, { bar: 2 }]},
169+
validStyleAttribute
170+
)).toEqual(null);
171+
172+
expect(diff(
173+
{ someStyle: [{}, { foo: 3, bar: 2 }]},
174+
{ someStyle: [{ foo: 1, bar: 1 }, { bar: 2 }]},
175+
validStyleAttribute
176+
)).toEqual({ foo: 1 });
177+
});
178+
179+
it('should clear a prop if a later style is explicit null/undefined', () => {
180+
var validStyleAttribute = { someStyle: { foo: true, bar: true } };
181+
expect(diff(
182+
{ someStyle: [{}, { foo: 3, bar: 2 }]},
183+
{ someStyle: [{ foo: 1 }, { bar: 2, foo: null }]},
184+
validStyleAttribute
185+
)).toEqual({ foo: null });
186+
187+
expect(diff(
188+
{ someStyle: [{ foo: 3 }, { foo: null, bar: 2 }]},
189+
{ someStyle: [{ foo: null }, { bar: 2 }]},
190+
validStyleAttribute
191+
)).toEqual(null);
192+
193+
expect(diff(
194+
{ someStyle: [{ foo: 1 }, { foo: null }]},
195+
{ someStyle: [{ foo: 2 }, { foo: null }]},
196+
validStyleAttribute
197+
)).toEqual(null);
198+
199+
// Test the same case with object equality because an early bailout doesn't
200+
// work in this case.
201+
var fooObj = { foo: 3 };
202+
expect(diff(
203+
{ someStyle: [{ foo: 1 }, fooObj]},
204+
{ someStyle: [{ foo: 2 }, fooObj]},
205+
validStyleAttribute
206+
)).toEqual(null);
207+
208+
expect(diff(
209+
{ someStyle: [{ foo: 1 }, { foo: 3 }]},
210+
{ someStyle: [{ foo: 2 }, { foo: undefined }]},
211+
validStyleAttribute
212+
)).toEqual({ foo: null });
213+
});
214+
125215
// Function properties are just markers to native that events should be sent.
126216
it('should convert functions to booleans', () => {
127217
// Note that if the property changes from one function to another, we don't

Libraries/StyleSheet/TransformPropTypes.js

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,22 @@
1313

1414
var ReactPropTypes = require('ReactPropTypes');
1515

16+
var ArrayOfNumberPropType = ReactPropTypes.arrayOf(ReactPropTypes.number);
17+
18+
var TransformMatrixPropType = function(
19+
props : Object,
20+
propName : string,
21+
componentName : string
22+
) : ?Error {
23+
if (props.transform && props.transformMatrix) {
24+
return new Error(
25+
'transformMatrix and transform styles cannot be used on the same ' +
26+
'component'
27+
);
28+
}
29+
return ArrayOfNumberPropType(props, propName, componentName);
30+
};
31+
1632
var TransformPropTypes = {
1733
transform: ReactPropTypes.arrayOf(
1834
ReactPropTypes.oneOfType([
@@ -30,7 +46,7 @@ var TransformPropTypes = {
3046
ReactPropTypes.shape({skewY: ReactPropTypes.string})
3147
])
3248
),
33-
transformMatrix: ReactPropTypes.arrayOf(ReactPropTypes.number),
49+
transformMatrix: TransformMatrixPropType,
3450
};
3551

3652
module.exports = TransformPropTypes;
Lines changed: 5 additions & 70 deletions
Original file line numberDiff line numberDiff line change
@@ -6,74 +6,17 @@
66
* LICENSE file in the root directory of this source tree. An additional grant
77
* of patent rights can be found in the PATENTS file in the same directory.
88
*
9-
* @providesModule precomputeStyle
9+
* @providesModule processTransform
1010
* @flow
1111
*/
1212
'use strict';
1313

1414
var MatrixMath = require('MatrixMath');
15-
var ReactNativeStyleAttributes = require('ReactNativeStyleAttributes');
1615
var Platform = require('Platform');
1716

18-
var deepFreezeAndThrowOnMutationInDev = require('deepFreezeAndThrowOnMutationInDev');
1917
var invariant = require('invariant');
2018
var stringifySafe = require('stringifySafe');
2119

22-
/**
23-
* This method provides a hook where flattened styles may be precomputed or
24-
* otherwise prepared to become better input data for native code.
25-
*/
26-
function precomputeStyle(style: ?Object, validAttributes: Object): ?Object {
27-
if (!style) {
28-
return style;
29-
}
30-
31-
var hasPreprocessKeys = false;
32-
for (var i = 0, keys = Object.keys(style); i < keys.length; i++) {
33-
var key = keys[i];
34-
if (_processor(key, validAttributes)) {
35-
hasPreprocessKeys = true;
36-
break;
37-
}
38-
}
39-
40-
if (!hasPreprocessKeys && !style.transform) {
41-
return style;
42-
}
43-
44-
var newStyle = {...style};
45-
for (var i = 0, keys = Object.keys(style); i < keys.length; i++) {
46-
var key = keys[i];
47-
var process = _processor(key, validAttributes);
48-
if (process) {
49-
newStyle[key] = process(newStyle[key]);
50-
}
51-
}
52-
53-
if (style.transform) {
54-
invariant(
55-
!style.transformMatrix,
56-
'transformMatrix and transform styles cannot be used on the same component'
57-
);
58-
59-
newStyle = _precomputeTransforms(newStyle);
60-
}
61-
62-
deepFreezeAndThrowOnMutationInDev(newStyle);
63-
return newStyle;
64-
}
65-
66-
function _processor(key: string, validAttributes: Object) {
67-
var process = validAttributes[key] && validAttributes[key].process;
68-
if (!process) {
69-
process =
70-
ReactNativeStyleAttributes[key] &&
71-
ReactNativeStyleAttributes[key].process;
72-
}
73-
74-
return process;
75-
}
76-
7720
/**
7821
* Generate a transform matrix based on the provided transforms, and use that
7922
* within the style object instead.
@@ -82,8 +25,7 @@ function _processor(key: string, validAttributes: Object) {
8225
* be applied in an arbitrary order, and yet have a universal, singular
8326
* interface to native code.
8427
*/
85-
function _precomputeTransforms(style: Object): Object {
86-
var {transform} = style;
28+
function processTransform(transform: Object): Object {
8729
var result = MatrixMath.createIdentityMatrix();
8830

8931
transform.forEach(transformation => {
@@ -144,16 +86,9 @@ function _precomputeTransforms(style: Object): Object {
14486
// get applied in the specific order of (1) translate (2) scale (3) rotate.
14587
// Once we can directly apply a matrix, we can remove this decomposition.
14688
if (Platform.OS === 'android') {
147-
return {
148-
...style,
149-
transformMatrix: result,
150-
decomposedMatrix: MatrixMath.decomposeMatrix(result),
151-
};
89+
return MatrixMath.decomposeMatrix(result);
15290
}
153-
return {
154-
...style,
155-
transformMatrix: result,
156-
};
91+
return result;
15792
}
15893

15994
/**
@@ -240,4 +175,4 @@ function _validateTransform(key, value, transformation) {
240175
}
241176
}
242177

243-
module.exports = precomputeStyle;
178+
module.exports = processTransform;

0 commit comments

Comments
 (0)