Spencer Ahrens 4ce03582a0 Improve Text perf
Summary:
public

Most apps create tons of text components but they are actually quite heavy because of the the Touchable mixin which requires binding tons of functions for every instance created.

This diff makes the binding lazy, so that the main handlers are only bound if there is a valid touch action configured (e.g. onPress), and the Touchable mixin functions are only bound the first time the node is actually touched and becomes the responder.

ScanLab testing shows 5-10% win on render time and memory for various products.

Reviewed By: sebmarkbage

Differential Revision: D2716823

fb-gh-sync-id: 30adb2ed2231c5635c9336369616cf31c776b930
2015-12-07 23:12:28 -08:00

251 lines
7.8 KiB
JavaScript

/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*
* @providesModule Text
* @flow
*/
'use strict';
const NativeMethodsMixin = require('NativeMethodsMixin');
const Platform = require('Platform');
const React = require('React');
const ReactInstanceMap = require('ReactInstanceMap');
const ReactNativeViewAttributes = require('ReactNativeViewAttributes');
const StyleSheetPropType = require('StyleSheetPropType');
const TextStylePropTypes = require('TextStylePropTypes');
const Touchable = require('Touchable');
const createReactNativeComponentClass =
require('createReactNativeComponentClass');
const merge = require('merge');
const stylePropType = StyleSheetPropType(TextStylePropTypes);
const viewConfig = {
validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
numberOfLines: true,
allowFontScaling: true,
}),
uiViewClassName: 'RCTText',
};
/**
* A React component for displaying text which supports nesting,
* styling, and touch handling. In the following example, the nested title and
* body text will inherit the `fontFamily` from `styles.baseText`, but the title
* provides its own additional styles. The title and body will stack on top of
* each other on account of the literal newlines:
*
* ```
* renderText: function() {
* return (
* <Text style={styles.baseText}>
* <Text style={styles.titleText} onPress={this.onPressTitle}>
* {this.state.titleText + '\n\n'}
* </Text>
* <Text numberOfLines={5}>
* {this.state.bodyText}
* </Text>
* </Text>
* );
* },
* ...
* var styles = StyleSheet.create({
* baseText: {
* fontFamily: 'Cochin',
* },
* titleText: {
* fontSize: 20,
* fontWeight: 'bold',
* },
* };
* ```
*/
const Text = React.createClass({
propTypes: {
/**
* Used to truncate the text with an elipsis after computing the text
* layout, including line wrapping, such that the total number of lines
* does not exceed this number.
*/
numberOfLines: React.PropTypes.number,
/**
* Invoked on mount and layout changes with
*
* `{nativeEvent: {layout: {x, y, width, height}}}`
*/
onLayout: React.PropTypes.func,
/**
* This function is called on press.
*/
onPress: React.PropTypes.func,
/**
* When true, no visual change is made when text is pressed down. By
* default, a gray oval highlights the text on press down.
* @platform ios
*/
suppressHighlighting: React.PropTypes.bool,
style: stylePropType,
/**
* Used to locate this view in end-to-end tests.
*/
testID: React.PropTypes.string,
/**
* Specifies should fonts scale to respect Text Size accessibility setting on iOS.
*/
allowFontScaling: React.PropTypes.bool,
},
getDefaultProps(): Object {
return {
accessible: true,
allowFontScaling: true,
};
},
getInitialState: function(): Object {
return merge(Touchable.Mixin.touchableGetInitialState(), {
isHighlighted: false,
});
},
mixins: [NativeMethodsMixin],
viewConfig: viewConfig,
getChildContext(): Object {
return {isInAParentText: true};
},
childContextTypes: {
isInAParentText: React.PropTypes.bool
},
contextTypes: {
isInAParentText: React.PropTypes.bool
},
/**
* Only assigned if touch is needed.
*/
_handlers: (null: ?Object),
/**
* These are assigned lazily the first time the responder is set to make plain
* text nodes as cheap as possible.
*/
touchableHandleActivePressIn: (null: ?Function),
touchableHandleActivePressOut: (null: ?Function),
touchableHandlePress: (null: ?Function),
touchableGetPressRectOffset: (null: ?Function),
render(): ReactElement {
let newProps = this.props;
if (this.props.onStartShouldSetResponder || this.props.onPress) {
if (!this._handlers) {
this._handlers = {
onStartShouldSetResponder: (): bool => {
const shouldSetFromProps = this.props.onStartShouldSetResponder &&
this.props.onStartShouldSetResponder();
const setResponder = shouldSetFromProps || !!this.props.onPress;
if (setResponder && !this.touchableHandleActivePressIn) {
// Attach and bind all the other handlers only the first time a touch
// actually happens.
for (let key in Touchable.Mixin) {
if (typeof Touchable.Mixin[key] === 'function') {
(this: any)[key] = Touchable.Mixin[key].bind(this);
}
}
this.touchableHandleActivePressIn = () => {
if (this.props.suppressHighlighting || !this.props.onPress) {
return;
}
this.setState({
isHighlighted: true,
});
};
this.touchableHandleActivePressOut = () => {
if (this.props.suppressHighlighting || !this.props.onPress) {
return;
}
this.setState({
isHighlighted: false,
});
};
this.touchableHandlePress = () => {
this.props.onPress && this.props.onPress();
};
this.touchableGetPressRectOffset = function(): RectOffset {
return PRESS_RECT_OFFSET;
};
}
return setResponder;
},
onResponderGrant: (e: SyntheticEvent, dispatchID: string) => {
this.touchableHandleResponderGrant(e, dispatchID);
this.props.onResponderGrant &&
this.props.onResponderGrant.apply(this, arguments);
},
onResponderMove: (e: SyntheticEvent) => {
this.touchableHandleResponderMove(e);
this.props.onResponderMove &&
this.props.onResponderMove.apply(this, arguments);
},
onResponderRelease: (e: SyntheticEvent) => {
this.touchableHandleResponderRelease(e);
this.props.onResponderRelease &&
this.props.onResponderRelease.apply(this, arguments);
},
onResponderTerminate: (e: SyntheticEvent) => {
this.touchableHandleResponderTerminate(e);
this.props.onResponderTerminate &&
this.props.onResponderTerminate.apply(this, arguments);
},
onResponderTerminationRequest: (): bool => {
// Allow touchable or props.onResponderTerminationRequest to deny
// the request
var allowTermination = this.touchableHandleResponderTerminationRequest();
if (allowTermination && this.props.onResponderTerminationRequest) {
allowTermination = this.props.onResponderTerminationRequest();
}
return allowTermination;
},
};
}
newProps = {
...this.props,
...this._handlers,
isHighlighted: this.state.isHighlighted,
};
}
if (this.context.isInAParentText) {
return <RCTVirtualText {...newProps} />;
} else {
return <RCTText {...newProps} />;
}
},
});
type RectOffset = {
top: number;
left: number;
right: number;
bottom: number;
}
var PRESS_RECT_OFFSET = {top: 20, left: 20, right: 20, bottom: 30};
var RCTText = createReactNativeComponentClass(viewConfig);
var RCTVirtualText = RCTText;
if (Platform.OS === 'android') {
RCTVirtualText = createReactNativeComponentClass({
validAttributes: merge(ReactNativeViewAttributes.UIView, {
isHighlighted: true,
}),
uiViewClassName: 'RCTVirtualText',
});
}
module.exports = Text;