Share reconcile transaction in batched updates

Should make #1350 better and will also take away any performance hit from #1157.

Test Plan:
grunt test
This commit is contained in:
Ben Alpert
2014-04-09 01:42:27 -07:00
parent bff6d50796
commit ddf53f97f0
5 changed files with 142 additions and 50 deletions
+3
View File
@@ -103,6 +103,9 @@ function inject() {
ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.script);
ReactInjection.Updates.injectReconcileTransaction(
ReactComponentBrowserEnvironment.ReactReconcileTransaction
);
ReactInjection.Updates.injectBatchingStrategy(
ReactDefaultBatchingStrategy
);
+4 -25
View File
@@ -101,8 +101,6 @@ var ReactComponent = {
ReactComponentEnvironment.unmountIDFromEnvironment;
ReactComponent.BackendIDOperations =
ReactComponentEnvironment.BackendIDOperations;
ReactComponent.ReactReconcileTransaction =
ReactComponentEnvironment.ReactReconcileTransaction;
injected = true;
}
},
@@ -121,14 +119,6 @@ var ReactComponent = {
*/
BackendIDOperations: null,
/**
* React references `ReactReconcileTransaction` using this property in order
* to allow dependency injection.
*
* @internal
*/
ReactReconcileTransaction: null,
/**
* Base functionality for every ReactComponent constructor. Mixed into the
* `ReactComponent` prototype, but exposed statically for easy access.
@@ -321,18 +311,7 @@ var ReactComponent = {
'receiveComponent(...): Can only update a mounted component.'
);
this._pendingDescriptor = nextDescriptor;
this._performUpdateIfNecessary(transaction);
},
/**
* Call `_performUpdateIfNecessary` within a new transaction.
*
* @internal
*/
performUpdateIfNecessary: function() {
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
transaction.perform(this._performUpdateIfNecessary, this, transaction);
ReactComponent.ReactReconcileTransaction.release(transaction);
this.performUpdateIfNecessary(transaction);
},
/**
@@ -341,7 +320,7 @@ var ReactComponent = {
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_performUpdateIfNecessary: function(transaction) {
performUpdateIfNecessary: function(transaction) {
if (this._pendingDescriptor == null) {
return;
}
@@ -405,7 +384,7 @@ var ReactComponent = {
* @see {ReactMount.renderComponent}
*/
mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
transaction.perform(
this._mountComponentIntoNode,
this,
@@ -414,7 +393,7 @@ var ReactComponent = {
transaction,
shouldReuseMarkup
);
ReactComponent.ReactReconcileTransaction.release(transaction);
ReactUpdates.ReactReconcileTransaction.release(transaction);
},
/**
+9 -12
View File
@@ -932,17 +932,6 @@ var ReactCompositeComponentMixin = {
}
},
performUpdateIfNecessary: function() {
var compositeLifeCycleState = this._compositeLifeCycleState;
// Do not trigger a state transition if we are in the middle of mounting or
// receiving props because both of those will already be doing this.
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
return;
}
ReactComponent.Mixin.performUpdateIfNecessary.call(this);
},
/**
* If any of `_pendingDescriptor`, `_pendingState`, or `_pendingForceUpdate`
* is set, update the component.
@@ -950,7 +939,15 @@ var ReactCompositeComponentMixin = {
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_performUpdateIfNecessary: function(transaction) {
performUpdateIfNecessary: function(transaction) {
var compositeLifeCycleState = this._compositeLifeCycleState;
// Do not trigger a state transition if we are in the middle of mounting or
// receiving props because both of those will already be doing this.
if (compositeLifeCycleState === CompositeLifeCycle.MOUNTING ||
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
return;
}
if (this._pendingDescriptor == null &&
this._pendingState == null &&
!this._pendingForceUpdate) {
+54 -13
View File
@@ -26,12 +26,16 @@ var dirtyComponents = [];
var batchingStrategy = null;
function ensureBatchingStrategy() {
invariant(batchingStrategy, 'ReactUpdates: must inject a batching strategy');
function ensureInjected() {
invariant(
ReactUpdates.ReactReconcileTransaction && batchingStrategy,
'ReactUpdates: must inject a reconcile transaction class and batching ' +
'strategy'
);
}
function batchedUpdates(callback, param) {
ensureBatchingStrategy();
ensureInjected();
batchingStrategy.batchedUpdates(callback, param);
}
@@ -46,7 +50,7 @@ function mountDepthComparator(c1, c2) {
return c1._mountDepth - c2._mountDepth;
}
function runBatchedUpdates() {
function runBatchedUpdates(transaction) {
// 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.
@@ -63,10 +67,14 @@ function runBatchedUpdates() {
// stash the callbacks first
var callbacks = component._pendingCallbacks;
component._pendingCallbacks = null;
component.performUpdateIfNecessary();
component.performUpdateIfNecessary(transaction);
if (callbacks) {
for (var j = 0; j < callbacks.length; j++) {
callbacks[j].call(component);
transaction.getReactMountReady().enqueue(
callbacks[j],
component
);
}
}
}
@@ -77,15 +85,26 @@ function clearDirtyComponents() {
dirtyComponents.length = 0;
}
function flushBatchedUpdatesOnce(transaction) {
// Run these in separate functions so the JIT can optimize
try {
runBatchedUpdates(transaction);
} finally {
clearDirtyComponents();
}
}
var flushBatchedUpdates = ReactPerf.measure(
'ReactUpdates',
'flushBatchedUpdates',
function() {
// Run these in separate functions so the JIT can optimize
try {
runBatchedUpdates();
} finally {
clearDirtyComponents();
// flushBatchedUpdatesOnce will clear the dirtyComponents array, but
// mount-ready handlers (i.e., componentDidMount/Update) may enqueue more
// state updates which we should apply immediately
while (dirtyComponents.length) {
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
transaction.perform(flushBatchedUpdatesOnce, null, transaction);
ReactUpdates.ReactReconcileTransaction.release(transaction);
}
}
);
@@ -101,10 +120,16 @@ function enqueueUpdate(component, callback) {
'`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
'isn\'t callable.'
);
ensureBatchingStrategy();
ensureInjected();
if (!batchingStrategy.isBatchingUpdates) {
component.performUpdateIfNecessary();
var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
transaction.perform(
component.performUpdateIfNecessary,
component,
transaction
);
ReactUpdates.ReactReconcileTransaction.release(transaction);
callback && callback.call(component);
return;
}
@@ -121,6 +146,14 @@ function enqueueUpdate(component, callback) {
}
var ReactUpdatesInjection = {
injectReconcileTransaction: function(ReconcileTransaction) {
invariant(
ReconcileTransaction,
'ReactUpdates: must provide a reconcile transaction class'
);
ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
},
injectBatchingStrategy: function(_batchingStrategy) {
invariant(
_batchingStrategy,
@@ -139,6 +172,14 @@ var ReactUpdatesInjection = {
};
var ReactUpdates = {
/**
* React references `ReactReconcileTransaction` using this property in order
* to allow dependency injection.
*
* @internal
*/
ReactReconcileTransaction: null,
batchedUpdates: batchedUpdates,
enqueueUpdate: enqueueUpdate,
flushBatchedUpdates: flushBatchedUpdates,
+72
View File
@@ -493,4 +493,76 @@ describe('ReactUpdates', function() {
['Box', 'Switcher', 'Child']
);
});
it('should share reconcile transaction across different roots', function() {
var ReconcileTransaction = ReactUpdates.ReactReconcileTransaction;
spyOn(ReconcileTransaction, 'getPooled').andCallThrough();
var Component = React.createClass({
render: function() {
return <div>{this.props.text}</div>;
}
});
var containerA = document.createElement('div');
var containerB = document.createElement('div');
// Initial renders aren't batched together yet...
ReactUpdates.batchedUpdates(function() {
React.renderComponent(<Component text="A1" />, containerA);
React.renderComponent(<Component text="B1" />, containerB);
});
expect(ReconcileTransaction.getPooled.calls.length).toBe(2);
// ...but updates are! Here only one more transaction is used, which means
// we only have to initialize and close the wrappers once.
ReactUpdates.batchedUpdates(function() {
React.renderComponent(<Component text="A2" />, containerA);
React.renderComponent(<Component text="B2" />, containerB);
});
expect(ReconcileTransaction.getPooled.calls.length).toBe(3);
});
it('should queue mount-ready handlers across different roots', function() {
// We'll define two components A and B, then update both of them. When A's
// componentDidUpdate handlers is called, B's DOM should already have been
// updated.
var a;
var b;
var aUpdated = false;
var A = React.createClass({
getInitialState: function() {
return {x: 0};
},
componentDidUpdate: function() {
expect(b.getDOMNode().textContent).toBe("B1");
aUpdated = true;
},
render: function() {
return <div>A{this.state.x}</div>;
}
});
var B = React.createClass({
getInitialState: function() {
return {x: 0};
},
render: function() {
return <div>B{this.state.x}</div>;
}
});
a = ReactTestUtils.renderIntoDocument(<A />);
b = ReactTestUtils.renderIntoDocument(<B />);
ReactUpdates.batchedUpdates(function() {
a.setState({x: 1});
b.setState({x: 1});
});
expect(aUpdated).toBe(true);
});
});