Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Share reconcile transaction in batched updates #1358

Merged
merged 1 commit into from
May 13, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/browser/ui/ReactDefaultInjection.js
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,9 @@ function inject() {

ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.script);

ReactInjection.Updates.injectReconcileTransaction(
ReactComponentBrowserEnvironment.ReactReconcileTransaction
);
ReactInjection.Updates.injectBatchingStrategy(
ReactDefaultBatchingStrategy
);
Expand Down
29 changes: 4 additions & 25 deletions src/core/ReactComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,6 @@ var ReactComponent = {
ReactComponentEnvironment.unmountIDFromEnvironment;
ReactComponent.BackendIDOperations =
ReactComponentEnvironment.BackendIDOperations;
ReactComponent.ReactReconcileTransaction =
ReactComponentEnvironment.ReactReconcileTransaction;
injected = true;
}
},
Expand All @@ -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.
Expand Down Expand Up @@ -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);
},

/**
Expand All @@ -341,7 +320,7 @@ var ReactComponent = {
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_performUpdateIfNecessary: function(transaction) {
performUpdateIfNecessary: function(transaction) {
if (this._pendingDescriptor == null) {
return;
}
Expand Down Expand Up @@ -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,
Expand All @@ -414,7 +393,7 @@ var ReactComponent = {
transaction,
shouldReuseMarkup
);
ReactComponent.ReactReconcileTransaction.release(transaction);
ReactUpdates.ReactReconcileTransaction.release(transaction);
},

/**
Expand Down
19 changes: 8 additions & 11 deletions src/core/ReactCompositeComponent.js
Original file line number Diff line number Diff line change
Expand Up @@ -932,25 +932,22 @@ var ReactCompositeComponentMixin = {
}
},

performUpdateIfNecessary: function() {
/**
* If any of `_pendingDescriptor`, `_pendingState`, or `_pendingForceUpdate`
* is set, update the component.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
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;
}
ReactComponent.Mixin.performUpdateIfNecessary.call(this);
},

/**
* If any of `_pendingDescriptor`, `_pendingState`, or `_pendingForceUpdate`
* is set, update the component.
*
* @param {ReactReconcileTransaction} transaction
* @internal
*/
_performUpdateIfNecessary: function(transaction) {
if (this._pendingDescriptor == null &&
this._pendingState == null &&
!this._pendingForceUpdate) {
Expand Down
67 changes: 54 additions & 13 deletions src/core/ReactUpdates.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was going to complain and say that this doesn't really need ReactReconcileTransaction to be injected. And we had a bunch of tests which were failing because they only inject the batching strategy (some flux-related code uses ReactUpdates directly). But then I just made those tests use ReactDefaultInjection instead.

ensureBatchingStrategy();
ensureInjected();
batchingStrategy.batchedUpdates(callback, param);
}

Expand All @@ -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.
Expand All @@ -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
);
}
}
}
Expand All @@ -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);
}
}
);
Expand All @@ -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;
}
Expand All @@ -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,
Expand All @@ -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,
Expand Down
72 changes: 72 additions & 0 deletions src/core/__tests__/ReactUpdates-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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);
});
});