mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
Sort batched updates by owner depth
If we reconcile components higher in the hierarchy they will likely reconcile components lower in the hierarchy. If we sort by depth then when we reach those components there will be no more pending state or props and it will no op.
This commit is contained in:
committed by
Paul O’Shannessy
parent
8beaa211fb
commit
58de758a32
@@ -335,10 +335,11 @@ var ReactComponent = {
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {number} mountDepth number of components in the owner hierarchy.
|
||||
* @return {?string} Rendered markup to be inserted into the DOM.
|
||||
* @internal
|
||||
*/
|
||||
mountComponent: function(rootID, transaction) {
|
||||
mountComponent: function(rootID, transaction, mountDepth) {
|
||||
invariant(
|
||||
!this.isMounted(),
|
||||
'mountComponent(%s, ...): Can only mount an unmounted component.',
|
||||
@@ -350,6 +351,7 @@ var ReactComponent = {
|
||||
}
|
||||
this._rootNodeID = rootID;
|
||||
this._lifeCycleState = ComponentLifeCycle.MOUNTED;
|
||||
this._mountDepth = mountDepth;
|
||||
// Effectively: return '';
|
||||
},
|
||||
|
||||
@@ -485,7 +487,7 @@ var ReactComponent = {
|
||||
container,
|
||||
transaction,
|
||||
shouldReuseMarkup) {
|
||||
var markup = this.mountComponent(rootID, transaction);
|
||||
var markup = this.mountComponent(rootID, transaction, 0);
|
||||
ReactComponent.mountImageIntoNode(markup, container, shouldReuseMarkup);
|
||||
},
|
||||
|
||||
|
||||
@@ -531,6 +531,7 @@ var ReactCompositeComponentMixin = {
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {number} mountDepth number of components in the owner hierarchy
|
||||
* @return {?string} Rendered markup to be inserted into the DOM.
|
||||
* @final
|
||||
* @internal
|
||||
@@ -538,8 +539,13 @@ var ReactCompositeComponentMixin = {
|
||||
mountComponent: ReactPerf.measure(
|
||||
'ReactCompositeComponent',
|
||||
'mountComponent',
|
||||
function(rootID, transaction) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
|
||||
function(rootID, transaction, mountDepth) {
|
||||
ReactComponent.Mixin.mountComponent.call(
|
||||
this,
|
||||
rootID,
|
||||
transaction,
|
||||
mountDepth
|
||||
);
|
||||
this._compositeLifeCycleState = CompositeLifeCycle.MOUNTING;
|
||||
|
||||
this._defaultProps = this.getDefaultProps ? this.getDefaultProps() : null;
|
||||
@@ -567,7 +573,11 @@ var ReactCompositeComponentMixin = {
|
||||
|
||||
// Done with mounting, `setState` will now trigger UI changes.
|
||||
this._compositeLifeCycleState = null;
|
||||
var markup = this._renderedComponent.mountComponent(rootID, transaction);
|
||||
var markup = this._renderedComponent.mountComponent(
|
||||
rootID,
|
||||
transaction,
|
||||
mountDepth + 1
|
||||
);
|
||||
if (this.componentDidMount) {
|
||||
transaction.getReactOnDOMReady().enqueue(this, this.componentDidMount);
|
||||
}
|
||||
@@ -788,7 +798,11 @@ var ReactCompositeComponentMixin = {
|
||||
var thisID = this._rootNodeID;
|
||||
var currentComponentID = currentComponent._rootNodeID;
|
||||
currentComponent.unmountComponent();
|
||||
var nextMarkup = nextComponent.mountComponent(thisID, transaction);
|
||||
var nextMarkup = nextComponent.mountComponent(
|
||||
thisID,
|
||||
transaction,
|
||||
this._mountDepth + 1
|
||||
);
|
||||
ReactComponent.DOMIDOperations.dangerouslyReplaceNodeWithMarkupByID(
|
||||
currentComponentID,
|
||||
nextMarkup
|
||||
|
||||
@@ -202,7 +202,11 @@ var ReactMultiChild = {
|
||||
if (children.hasOwnProperty(name) && child) {
|
||||
// Inlined for performance, see `ReactInstanceHandles.createReactID`.
|
||||
var rootID = this._rootNodeID + '.' + name;
|
||||
var mountImage = child.mountComponent(rootID, transaction);
|
||||
var mountImage = child.mountComponent(
|
||||
rootID,
|
||||
transaction,
|
||||
this._mountDepth + 1
|
||||
);
|
||||
child._mountImage = mountImage;
|
||||
child._mountIndex = index;
|
||||
mountImages.push(mountImage);
|
||||
@@ -395,7 +399,11 @@ var ReactMultiChild = {
|
||||
_mountChildByNameAtIndex: function(child, name, index, transaction) {
|
||||
// Inlined for performance, see `ReactInstanceHandles.createReactID`.
|
||||
var rootID = this._rootNodeID + '.' + name;
|
||||
var mountImage = child.mountComponent(rootID, transaction);
|
||||
var mountImage = child.mountComponent(
|
||||
rootID,
|
||||
transaction,
|
||||
this._mountDepth + 1
|
||||
);
|
||||
child._mountImage = mountImage;
|
||||
child._mountIndex = index;
|
||||
this.createChild(child);
|
||||
|
||||
@@ -84,13 +84,19 @@ ReactNativeComponent.Mixin = {
|
||||
* @internal
|
||||
* @param {string} rootID The root DOM ID for this node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {number} mountDepth number of components in the owner hierarchy
|
||||
* @return {string} The computed markup.
|
||||
*/
|
||||
mountComponent: ReactPerf.measure(
|
||||
'ReactNativeComponent',
|
||||
'mountComponent',
|
||||
function(rootID, transaction) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID, transaction);
|
||||
function(rootID, transaction, mountDepth) {
|
||||
ReactComponent.Mixin.mountComponent.call(
|
||||
this,
|
||||
rootID,
|
||||
transaction,
|
||||
mountDepth
|
||||
);
|
||||
assertValidProps(this.props);
|
||||
return (
|
||||
this._createOpenTagMarkup() +
|
||||
|
||||
@@ -52,11 +52,18 @@ mixInto(ReactTextComponent, {
|
||||
* any features besides containing text content.
|
||||
*
|
||||
* @param {string} rootID DOM ID of the root node.
|
||||
* @param {ReactReconcileTransaction} transaction
|
||||
* @param {number} mountDepth number of components in the owner hierarchy
|
||||
* @return {string} Markup for this text node.
|
||||
* @internal
|
||||
*/
|
||||
mountComponent: function(rootID) {
|
||||
ReactComponent.Mixin.mountComponent.call(this, rootID);
|
||||
mountComponent: function(rootID, transaction, mountDepth) {
|
||||
ReactComponent.Mixin.mountComponent.call(
|
||||
this,
|
||||
rootID,
|
||||
transaction,
|
||||
mountDepth
|
||||
);
|
||||
return (
|
||||
'<span ' + ReactMount.ATTR_NAME + '="' + rootID + '">' +
|
||||
escapeTextForBrowser(this.props.text) +
|
||||
|
||||
@@ -33,8 +33,24 @@ function batchedUpdates(callback, param) {
|
||||
batchingStrategy.batchedUpdates(callback, param);
|
||||
}
|
||||
|
||||
/**
|
||||
* Array comparator for ReactComponents by owner depth
|
||||
*
|
||||
* @param {ReactComponent} c1 first component you're comparing
|
||||
* @param {ReactComponent} c2 second component you're comparing
|
||||
* @return {number} Return value usable by Array.prototype.sort().
|
||||
*/
|
||||
function mountDepthComparator(c1, c2) {
|
||||
return c1._mountDepth - c2._mountDepth;
|
||||
}
|
||||
|
||||
function runBatchedUpdates() {
|
||||
// TODO: Sort components by depth such that parent components update first
|
||||
// Since reconciling a component higher in the owner hierarchy usually (not
|
||||
// always -- see shouldComponentUpdate()) will reconcile children, reconcile
|
||||
// them before their children by sorting the array.
|
||||
|
||||
dirtyComponents.sort(mountDepthComparator);
|
||||
|
||||
for (var i = 0; i < dirtyComponents.length; i++) {
|
||||
// If a component is unmounted before pending changes apply, ignore them
|
||||
// TODO: Queue unmounts in the same list to avoid this happening at all
|
||||
|
||||
@@ -133,4 +133,79 @@ describe('ReactComponent', function() {
|
||||
expect(instance.isMounted()).toBeTruthy();
|
||||
});
|
||||
|
||||
it('should know its simple mount depth', function() {
|
||||
var Owner = React.createClass({
|
||||
render: function() {
|
||||
return <Child ref="child" />;
|
||||
}
|
||||
});
|
||||
|
||||
var Child = React.createClass({
|
||||
render: function() {
|
||||
return <div />;
|
||||
}
|
||||
});
|
||||
|
||||
var instance = <Owner />;
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
expect(instance._mountDepth).toBe(0);
|
||||
expect(instance.refs.child._mountDepth).toBe(1);
|
||||
});
|
||||
|
||||
it('should know its (complicated) mount depth', function() {
|
||||
var Box = React.createClass({
|
||||
render: function() {
|
||||
return <div ref="boxDiv">{this.props.children}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var Child = React.createClass({
|
||||
render: function() {
|
||||
return <span ref="span">child</span>;
|
||||
}
|
||||
});
|
||||
|
||||
var Switcher = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {tabKey: 'hello'};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var child = this.props.children;
|
||||
|
||||
return (
|
||||
<Box ref="box">
|
||||
<div
|
||||
ref="switcherDiv"
|
||||
style={{
|
||||
display: this.state.tabKey === child.key ? '' : 'none'
|
||||
}}>
|
||||
{child}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var App = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<Switcher ref="switcher">
|
||||
<Child key="hello" ref="child" />
|
||||
</Switcher>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var root = <App />;
|
||||
ReactTestUtils.renderIntoDocument(root);
|
||||
|
||||
expect(root._mountDepth).toBe(0);
|
||||
expect(root.refs.switcher._mountDepth).toBe(1);
|
||||
expect(root.refs.switcher.refs.box._mountDepth).toBe(2);
|
||||
expect(root.refs.switcher.refs.switcherDiv._mountDepth).toBe(4);
|
||||
expect(root.refs.child._mountDepth).toBe(5);
|
||||
expect(root.refs.switcher.refs.box.refs.boxDiv._mountDepth).toBe(3);
|
||||
expect(root.refs.child.refs.span._mountDepth).toBe(6);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -290,7 +290,7 @@ describe('ReactNativeComponent', function() {
|
||||
mountComponent = function(props) {
|
||||
var transaction = new ReactReconcileTransaction();
|
||||
var stubComponent = new StubNativeComponent(props);
|
||||
return stubComponent.mountComponent('test', transaction);
|
||||
return stubComponent.mountComponent('test', transaction, 0);
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -210,10 +210,8 @@ describe('ReactUpdates', function() {
|
||||
expect(child.state.y).toBe(2);
|
||||
expect(parentUpdateCount).toBe(1);
|
||||
|
||||
// When we update the child first, we currently incur two updates because
|
||||
// we aren't smart about what order to process the components in.
|
||||
// TODO: Reduce the update count here to 1
|
||||
expect(childUpdateCount).toBe(2);
|
||||
// Batching reduces the number of updates here to 1.
|
||||
expect(childUpdateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should support chained state updates', function() {
|
||||
@@ -293,4 +291,169 @@ describe('ReactUpdates', function() {
|
||||
expect(instance.state.x).toBe(1);
|
||||
expect(updateCount).toBe(1);
|
||||
});
|
||||
|
||||
it('should update children even if parent blocks updates', function() {
|
||||
var parentRenderCount = 0;
|
||||
var childRenderCount = 0;
|
||||
|
||||
var Parent = React.createClass({
|
||||
shouldComponentUpdate: function() {
|
||||
return false;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
parentRenderCount++;
|
||||
return <Child ref="child" />;
|
||||
}
|
||||
});
|
||||
|
||||
var Child = React.createClass({
|
||||
render: function() {
|
||||
childRenderCount++;
|
||||
return <div />;
|
||||
}
|
||||
});
|
||||
|
||||
expect(parentRenderCount).toBe(0);
|
||||
expect(childRenderCount).toBe(0);
|
||||
|
||||
var instance = <Parent />;
|
||||
ReactTestUtils.renderIntoDocument(instance);
|
||||
|
||||
expect(parentRenderCount).toBe(1);
|
||||
expect(childRenderCount).toBe(1);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.setState({x: 1});
|
||||
});
|
||||
|
||||
expect(parentRenderCount).toBe(1);
|
||||
expect(childRenderCount).toBe(1);
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
instance.refs.child.setState({x: 1});
|
||||
});
|
||||
|
||||
expect(parentRenderCount).toBe(1);
|
||||
expect(childRenderCount).toBe(2);
|
||||
});
|
||||
|
||||
it('should flow updates correctly', function() {
|
||||
var willUpdates = [];
|
||||
var didUpdates = [];
|
||||
|
||||
var UpdateLoggingMixin = {
|
||||
componentWillUpdate: function() {
|
||||
willUpdates.push(this.constructor.displayName);
|
||||
},
|
||||
componentDidUpdate: function() {
|
||||
didUpdates.push(this.constructor.displayName);
|
||||
}
|
||||
};
|
||||
|
||||
var Box = React.createClass({
|
||||
mixins: [UpdateLoggingMixin],
|
||||
|
||||
render: function() {
|
||||
return <div ref="boxDiv">{this.props.children}</div>;
|
||||
}
|
||||
});
|
||||
|
||||
var Child = React.createClass({
|
||||
mixins: [UpdateLoggingMixin],
|
||||
|
||||
render: function() {
|
||||
return <span ref="span">child</span>;
|
||||
}
|
||||
});
|
||||
|
||||
var Switcher = React.createClass({
|
||||
mixins: [UpdateLoggingMixin],
|
||||
|
||||
getInitialState: function() {
|
||||
return {tabKey: 'hello'};
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var child = this.props.children;
|
||||
|
||||
return (
|
||||
<Box ref="box">
|
||||
<div
|
||||
ref="switcherDiv"
|
||||
style={{
|
||||
display: this.state.tabKey === child.key ? '' : 'none'
|
||||
}}>
|
||||
{child}
|
||||
</div>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var App = React.createClass({
|
||||
mixins: [UpdateLoggingMixin],
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<Switcher ref="switcher">
|
||||
<Child key="hello" ref="child" />
|
||||
</Switcher>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var root = <App />;
|
||||
ReactTestUtils.renderIntoDocument(root);
|
||||
|
||||
function expectUpdates(sequence) {
|
||||
// didUpdate() occurs in reverse order
|
||||
didUpdates.reverse();
|
||||
expect(willUpdates).toEqual(didUpdates);
|
||||
expect(willUpdates).toEqual(sequence);
|
||||
willUpdates.length = 0;
|
||||
didUpdates.length = 0;
|
||||
}
|
||||
|
||||
function triggerUpdate(c) {
|
||||
c.setState({x: 1});
|
||||
}
|
||||
|
||||
function testUpdates(components, expectation) {
|
||||
var i;
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
for (i = 0; i < components.length; i++) {
|
||||
triggerUpdate(components[i]);
|
||||
}
|
||||
});
|
||||
|
||||
expectUpdates(expectation);
|
||||
|
||||
// Try them in reverse order
|
||||
|
||||
ReactUpdates.batchedUpdates(function() {
|
||||
for (i = components.length - 1; i >= 0; i--) {
|
||||
triggerUpdate(components[i]);
|
||||
}
|
||||
});
|
||||
|
||||
expectUpdates(expectation);
|
||||
}
|
||||
|
||||
testUpdates(
|
||||
[root.refs.switcher.refs.box, root.refs.switcher],
|
||||
['Switcher', 'Box', 'Child']
|
||||
);
|
||||
|
||||
testUpdates(
|
||||
[root.refs.child, root.refs.switcher.refs.box],
|
||||
['Box', 'Child']
|
||||
);
|
||||
|
||||
testUpdates(
|
||||
[root.refs.child, root.refs.switcher],
|
||||
['Switcher', 'Box', 'Child']
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,14 +36,18 @@ describe('Danger', function() {
|
||||
});
|
||||
|
||||
it('should render markup', function() {
|
||||
var markup = (<div />).mountComponent('.rX', transaction);
|
||||
var markup = (<div />).mountComponent('.rX', transaction, 0);
|
||||
var output = Danger.dangerouslyRenderMarkup([markup])[0];
|
||||
|
||||
expect(output.nodeName).toBe('DIV');
|
||||
});
|
||||
|
||||
it('should render markup with props', function() {
|
||||
var markup = (<div className="foo" />).mountComponent('.rX', transaction);
|
||||
var markup = (<div className="foo" />).mountComponent(
|
||||
'.rX',
|
||||
transaction,
|
||||
0
|
||||
);
|
||||
var output = Danger.dangerouslyRenderMarkup([markup])[0];
|
||||
|
||||
expect(output.nodeName).toBe('DIV');
|
||||
@@ -51,7 +55,7 @@ describe('Danger', function() {
|
||||
});
|
||||
|
||||
it('should render wrapped markup', function() {
|
||||
var markup = (<th />).mountComponent('.rX', transaction);
|
||||
var markup = (<th />).mountComponent('.rX', transaction, 0);
|
||||
var output = Danger.dangerouslyRenderMarkup([markup])[0];
|
||||
|
||||
expect(output.nodeName).toBe('TH');
|
||||
|
||||
@@ -34,7 +34,7 @@ function renderComponentToString(component, callback) {
|
||||
transaction.reinitializeTransaction();
|
||||
try {
|
||||
transaction.perform(function() {
|
||||
var markup = component.mountComponent(id, transaction);
|
||||
var markup = component.mountComponent(id, transaction, 0);
|
||||
markup = ReactMarkupChecksum.addChecksumToMarkup(markup);
|
||||
callback(markup);
|
||||
}, null);
|
||||
|
||||
Reference in New Issue
Block a user