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:
Pete Hunt
2013-10-01 13:33:12 -07:00
committed by Paul O’Shannessy
parent 8beaa211fb
commit 58de758a32
11 changed files with 317 additions and 22 deletions
+4 -2
View File
@@ -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);
},
+18 -4
View File
@@ -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
+10 -2
View File
@@ -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);
+8 -2
View File
@@ -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() +
+9 -2
View File
@@ -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) +
+17 -1
View File
@@ -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
+75
View File
@@ -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);
};
});
+167 -4
View File
@@ -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']
);
});
});
+7 -3
View File
@@ -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');
+1 -1
View File
@@ -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);