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
This commit is contained in:
parent
e25d5c2f37
commit
4ce03582a0
|
@ -11,22 +11,22 @@
|
|||
*/
|
||||
'use strict';
|
||||
|
||||
var NativeMethodsMixin = require('NativeMethodsMixin');
|
||||
var Platform = require('Platform');
|
||||
var React = require('React');
|
||||
var ReactInstanceMap = require('ReactInstanceMap');
|
||||
var ReactNativeViewAttributes = require('ReactNativeViewAttributes');
|
||||
var StyleSheetPropType = require('StyleSheetPropType');
|
||||
var TextStylePropTypes = require('TextStylePropTypes');
|
||||
var Touchable = require('Touchable');
|
||||
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');
|
||||
|
||||
var createReactNativeComponentClass =
|
||||
const createReactNativeComponentClass =
|
||||
require('createReactNativeComponentClass');
|
||||
var merge = require('merge');
|
||||
const merge = require('merge');
|
||||
|
||||
var stylePropType = StyleSheetPropType(TextStylePropTypes);
|
||||
const stylePropType = StyleSheetPropType(TextStylePropTypes);
|
||||
|
||||
var viewConfig = {
|
||||
const viewConfig = {
|
||||
validAttributes: merge(ReactNativeViewAttributes.UIView, {
|
||||
isHighlighted: true,
|
||||
numberOfLines: true,
|
||||
|
@ -68,10 +68,7 @@ var viewConfig = {
|
|||
* ```
|
||||
*/
|
||||
|
||||
var Text = React.createClass({
|
||||
|
||||
mixins: [Touchable.Mixin, NativeMethodsMixin],
|
||||
|
||||
const Text = React.createClass({
|
||||
propTypes: {
|
||||
/**
|
||||
* Used to truncate the text with an elipsis after computing the text
|
||||
|
@ -105,30 +102,106 @@ var Text = React.createClass({
|
|||
*/
|
||||
allowFontScaling: React.PropTypes.bool,
|
||||
},
|
||||
|
||||
viewConfig: viewConfig,
|
||||
|
||||
getInitialState: function(): Object {
|
||||
return merge(this.touchableGetInitialState(), {
|
||||
isHighlighted: false,
|
||||
});
|
||||
},
|
||||
getDefaultProps: function(): Object {
|
||||
getDefaultProps(): Object {
|
||||
return {
|
||||
accessible: true,
|
||||
allowFontScaling: true,
|
||||
};
|
||||
},
|
||||
|
||||
onStartShouldSetResponder: function(): bool {
|
||||
var shouldSetFromProps = this.props.onStartShouldSetResponder &&
|
||||
this.props.onStartShouldSetResponder();
|
||||
return shouldSetFromProps || !!this.props.onPress;
|
||||
getInitialState: function(): Object {
|
||||
return merge(Touchable.Mixin.touchableGetInitialState(), {
|
||||
isHighlighted: false,
|
||||
});
|
||||
},
|
||||
|
||||
/*
|
||||
* Returns true to allow responder termination
|
||||
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.
|
||||
*/
|
||||
handleResponderTerminationRequest: function(): bool {
|
||||
_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();
|
||||
|
@ -137,89 +210,18 @@ var Text = React.createClass({
|
|||
}
|
||||
return allowTermination;
|
||||
},
|
||||
|
||||
handleResponderGrant: function(e: SyntheticEvent, dispatchID: string) {
|
||||
this.touchableHandleResponderGrant(e, dispatchID);
|
||||
this.props.onResponderGrant &&
|
||||
this.props.onResponderGrant.apply(this, arguments);
|
||||
},
|
||||
|
||||
handleResponderMove: function(e: SyntheticEvent) {
|
||||
this.touchableHandleResponderMove(e);
|
||||
this.props.onResponderMove &&
|
||||
this.props.onResponderMove.apply(this, arguments);
|
||||
},
|
||||
|
||||
handleResponderRelease: function(e: SyntheticEvent) {
|
||||
this.touchableHandleResponderRelease(e);
|
||||
this.props.onResponderRelease &&
|
||||
this.props.onResponderRelease.apply(this, arguments);
|
||||
},
|
||||
|
||||
handleResponderTerminate: function(e: SyntheticEvent) {
|
||||
this.touchableHandleResponderTerminate(e);
|
||||
this.props.onResponderTerminate &&
|
||||
this.props.onResponderTerminate.apply(this, arguments);
|
||||
},
|
||||
|
||||
touchableHandleActivePressIn: function() {
|
||||
if (this.props.suppressHighlighting || !this.props.onPress) {
|
||||
return;
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
isHighlighted: true,
|
||||
});
|
||||
},
|
||||
|
||||
touchableHandleActivePressOut: function() {
|
||||
if (this.props.suppressHighlighting || !this.props.onPress) {
|
||||
return;
|
||||
newProps = {
|
||||
...this.props,
|
||||
...this._handlers,
|
||||
isHighlighted: this.state.isHighlighted,
|
||||
};
|
||||
}
|
||||
this.setState({
|
||||
isHighlighted: false,
|
||||
});
|
||||
},
|
||||
|
||||
touchableHandlePress: function() {
|
||||
this.props.onPress && this.props.onPress();
|
||||
},
|
||||
|
||||
touchableGetPressRectOffset: function(): RectOffset {
|
||||
return PRESS_RECT_OFFSET;
|
||||
},
|
||||
|
||||
getChildContext: function(): Object {
|
||||
return {isInAParentText: true};
|
||||
},
|
||||
|
||||
childContextTypes: {
|
||||
isInAParentText: React.PropTypes.bool
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var props = {};
|
||||
for (var key in this.props) {
|
||||
props[key] = this.props[key];
|
||||
}
|
||||
// Text is accessible by default
|
||||
if (props.accessible !== false) {
|
||||
props.accessible = true;
|
||||
}
|
||||
props.isHighlighted = this.state.isHighlighted;
|
||||
props.onStartShouldSetResponder = this.onStartShouldSetResponder;
|
||||
props.onResponderTerminationRequest =
|
||||
this.handleResponderTerminationRequest;
|
||||
props.onResponderGrant = this.handleResponderGrant;
|
||||
props.onResponderMove = this.handleResponderMove;
|
||||
props.onResponderRelease = this.handleResponderRelease;
|
||||
props.onResponderTerminate = this.handleResponderTerminate;
|
||||
|
||||
// TODO: Switch to use contextTypes and this.context after React upgrade
|
||||
var context = ReactInstanceMap.get(this)._context;
|
||||
if (context.isInAParentText) {
|
||||
return <RCTVirtualText {...props} />;
|
||||
if (this.context.isInAParentText) {
|
||||
return <RCTVirtualText {...newProps} />;
|
||||
} else {
|
||||
return <RCTText {...props} />;
|
||||
return <RCTText {...newProps} />;
|
||||
}
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue