Share reconcile transaction in batched updates
Should make #1350 better and will also take away any performance hit from #1157. Test Plan: grunt test
Ben Alpert committed
Apr 5, 2014 at 18:13 UTC
ddf53f97f09a652f173bc509b903ead920d92b52
5 files changed
+141
-49
src/browser/ui/ReactDefaultInjection.js
+3
@@ -103,6 +103,9 @@ function inject() {
103
104
ReactInjection.EmptyComponent.injectEmptyComponent(ReactDOM.script);
105
106
+ ReactInjection.Updates.injectReconcileTransaction(
107
+ ReactComponentBrowserEnvironment.ReactReconcileTransaction
108
+ );
109
ReactInjection.Updates.injectBatchingStrategy(
110
ReactDefaultBatchingStrategy
111
);
src/core/ReactComponent.js
+4
-25
@@ -101,8 +101,6 @@ var ReactComponent = {
101
ReactComponentEnvironment.unmountIDFromEnvironment;
102
ReactComponent.BackendIDOperations =
103
ReactComponentEnvironment.BackendIDOperations;
104
- ReactComponent.ReactReconcileTransaction =
105
- ReactComponentEnvironment.ReactReconcileTransaction;
104
injected = true;
105
}
106
},
@@ -121,14 +119,6 @@ var ReactComponent = {
119
*/
120
BackendIDOperations: null,
121
124
- /**
125
- * React references `ReactReconcileTransaction` using this property in order
126
- * to allow dependency injection.
127
- *
128
- * @internal
129
- */
130
- ReactReconcileTransaction: null,
131
-
122
/**
123
* Base functionality for every ReactComponent constructor. Mixed into the
124
* `ReactComponent` prototype, but exposed statically for easy access.
@@ -321,18 +311,7 @@ var ReactComponent = {
311
'receiveComponent(...): Can only update a mounted component.'
312
);
313
this._pendingDescriptor = nextDescriptor;
324
- this._performUpdateIfNecessary(transaction);
325
- },
326
-
327
- /**
328
- * Call `_performUpdateIfNecessary` within a new transaction.
329
- *
330
- * @internal
331
- */
332
- performUpdateIfNecessary: function() {
333
- var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
334
- transaction.perform(this._performUpdateIfNecessary, this, transaction);
335
- ReactComponent.ReactReconcileTransaction.release(transaction);
314
+ this.performUpdateIfNecessary(transaction);
315
},
316
317
/**
@@ -341,7 +320,7 @@ var ReactComponent = {
320
* @param {ReactReconcileTransaction} transaction
321
* @internal
322
*/
344
- _performUpdateIfNecessary: function(transaction) {
323
+ performUpdateIfNecessary: function(transaction) {
324
if (this._pendingDescriptor == null) {
325
return;
326
}
@@ -405,7 +384,7 @@ var ReactComponent = {
384
* @see {ReactMount.renderComponent}
385
*/
386
mountComponentIntoNode: function(rootID, container, shouldReuseMarkup) {
408
- var transaction = ReactComponent.ReactReconcileTransaction.getPooled();
387
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
388
transaction.perform(
389
this._mountComponentIntoNode,
390
this,
@@ -414,7 +393,7 @@ var ReactComponent = {
393
transaction,
394
shouldReuseMarkup
395
);
417
- ReactComponent.ReactReconcileTransaction.release(transaction);
396
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
397
},
398
399
/**
src/core/ReactCompositeComponent.js
+8
-11
@@ -932,7 +932,14 @@ var ReactCompositeComponentMixin = {
932
}
933
},
934
935
- performUpdateIfNecessary: function() {
935
+ /**
936
+ * If any of `_pendingDescriptor`, `_pendingState`, or `_pendingForceUpdate`
937
+ * is set, update the component.
938
+ *
939
+ * @param {ReactReconcileTransaction} transaction
940
+ * @internal
941
+ */
942
+ performUpdateIfNecessary: function(transaction) {
943
var compositeLifeCycleState = this._compositeLifeCycleState;
944
// Do not trigger a state transition if we are in the middle of mounting or
945
// receiving props because both of those will already be doing this.
@@ -940,17 +947,7 @@ var ReactCompositeComponentMixin = {
947
compositeLifeCycleState === CompositeLifeCycle.RECEIVING_PROPS) {
948
return;
949
}
943
- ReactComponent.Mixin.performUpdateIfNecessary.call(this);
944
- },
950
946
- /**
947
- * If any of `_pendingDescriptor`, `_pendingState`, or `_pendingForceUpdate`
948
- * is set, update the component.
949
- *
950
- * @param {ReactReconcileTransaction} transaction
951
- * @internal
952
- */
953
- _performUpdateIfNecessary: function(transaction) {
951
if (this._pendingDescriptor == null &&
952
this._pendingState == null &&
953
!this._pendingForceUpdate) {
src/core/ReactUpdates.js
+54
-13
@@ -26,12 +26,16 @@ var dirtyComponents = [];
26
27
var batchingStrategy = null;
28
29
-function ensureBatchingStrategy() {
30
- invariant(batchingStrategy, 'ReactUpdates: must inject a batching strategy');
29
+function ensureInjected() {
30
+ invariant(
31
+ ReactUpdates.ReactReconcileTransaction && batchingStrategy,
32
+ 'ReactUpdates: must inject a reconcile transaction class and batching ' +
33
+ 'strategy'
34
+ );
35
}
36
37
function batchedUpdates(callback, param) {
34
- ensureBatchingStrategy();
38
+ ensureInjected();
39
batchingStrategy.batchedUpdates(callback, param);
40
}
41
@@ -46,7 +50,7 @@ function mountDepthComparator(c1, c2) {
50
return c1._mountDepth - c2._mountDepth;
51
}
52
49
-function runBatchedUpdates() {
53
+function runBatchedUpdates(transaction) {
54
// Since reconciling a component higher in the owner hierarchy usually (not
55
// always -- see shouldComponentUpdate()) will reconcile children, reconcile
56
// them before their children by sorting the array.
@@ -63,10 +67,14 @@ function runBatchedUpdates() {
67
// stash the callbacks first
68
var callbacks = component._pendingCallbacks;
69
component._pendingCallbacks = null;
66
- component.performUpdateIfNecessary();
70
+ component.performUpdateIfNecessary(transaction);
71
+
72
if (callbacks) {
73
for (var j = 0; j < callbacks.length; j++) {
69
- callbacks[j].call(component);
74
+ transaction.getReactMountReady().enqueue(
75
+ callbacks[j],
76
+ component
77
+ );
78
}
79
}
80
}
@@ -77,15 +85,26 @@ function clearDirtyComponents() {
85
dirtyComponents.length = 0;
86
}
87
88
+function flushBatchedUpdatesOnce(transaction) {
89
+ // Run these in separate functions so the JIT can optimize
90
+ try {
91
+ runBatchedUpdates(transaction);
92
+ } finally {
93
+ clearDirtyComponents();
94
+ }
95
+}
96
+
97
var flushBatchedUpdates = ReactPerf.measure(
98
'ReactUpdates',
99
'flushBatchedUpdates',
100
function() {
84
- // Run these in separate functions so the JIT can optimize
85
- try {
86
- runBatchedUpdates();
87
- } finally {
88
- clearDirtyComponents();
101
+ // flushBatchedUpdatesOnce will clear the dirtyComponents array, but
102
+ // mount-ready handlers (i.e., componentDidMount/Update) may enqueue more
103
+ // state updates which we should apply immediately
104
+ while (dirtyComponents.length) {
105
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
106
+ transaction.perform(flushBatchedUpdatesOnce, null, transaction);
107
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
108
}
109
}
110
);
@@ -101,10 +120,16 @@ function enqueueUpdate(component, callback) {
120
'`setState`, `replaceState`, or `forceUpdate` with a callback that ' +
121
'isn\'t callable.'
122
);
104
- ensureBatchingStrategy();
123
+ ensureInjected();
124
125
if (!batchingStrategy.isBatchingUpdates) {
107
- component.performUpdateIfNecessary();
126
+ var transaction = ReactUpdates.ReactReconcileTransaction.getPooled();
127
+ transaction.perform(
128
+ component.performUpdateIfNecessary,
129
+ component,
130
+ transaction
131
+ );
132
+ ReactUpdates.ReactReconcileTransaction.release(transaction);
133
callback && callback.call(component);
134
return;
135
}
@@ -121,6 +146,14 @@ function enqueueUpdate(component, callback) {
146
}
147
148
var ReactUpdatesInjection = {
149
+ injectReconcileTransaction: function(ReconcileTransaction) {
150
+ invariant(
151
+ ReconcileTransaction,
152
+ 'ReactUpdates: must provide a reconcile transaction class'
153
+ );
154
+ ReactUpdates.ReactReconcileTransaction = ReconcileTransaction;
155
+ },
156
+
157
injectBatchingStrategy: function(_batchingStrategy) {
158
invariant(
159
_batchingStrategy,
@@ -139,6 +172,14 @@ var ReactUpdatesInjection = {
172
};
173
174
var ReactUpdates = {
175
+ /**
176
+ * React references `ReactReconcileTransaction` using this property in order
177
+ * to allow dependency injection.
178
+ *
179
+ * @internal
180
+ */
181
+ ReactReconcileTransaction: null,
182
+
183
batchedUpdates: batchedUpdates,
184
enqueueUpdate: enqueueUpdate,
185
flushBatchedUpdates: flushBatchedUpdates,
src/core/__tests__/ReactUpdates-test.js
+72
@@ -493,4 +493,76 @@ describe('ReactUpdates', function() {
493
['Box', 'Switcher', 'Child']
494
);
495
});
496
+
497
+ it('should share reconcile transaction across different roots', function() {
498
+ var ReconcileTransaction = ReactUpdates.ReactReconcileTransaction;
499
+ spyOn(ReconcileTransaction, 'getPooled').andCallThrough();
500
+
501
+ var Component = React.createClass({
502
+ render: function() {
503
+ return <div>{this.props.text}</div>;
504
+ }
505
+ });
506
+
507
+ var containerA = document.createElement('div');
508
+ var containerB = document.createElement('div');
509
+
510
+ // Initial renders aren't batched together yet...
511
+ ReactUpdates.batchedUpdates(function() {
512
+ React.renderComponent(<Component text="A1" />, containerA);
513
+ React.renderComponent(<Component text="B1" />, containerB);
514
+ });
515
+ expect(ReconcileTransaction.getPooled.calls.length).toBe(2);
516
+
517
+ // ...but updates are! Here only one more transaction is used, which means
518
+ // we only have to initialize and close the wrappers once.
519
+ ReactUpdates.batchedUpdates(function() {
520
+ React.renderComponent(<Component text="A2" />, containerA);
521
+ React.renderComponent(<Component text="B2" />, containerB);
522
+ });
523
+ expect(ReconcileTransaction.getPooled.calls.length).toBe(3);
524
+ });
525
+
526
+ it('should queue mount-ready handlers across different roots', function() {
527
+ // We'll define two components A and B, then update both of them. When A's
528
+ // componentDidUpdate handlers is called, B's DOM should already have been
529
+ // updated.
530
+
531
+ var a;
532
+ var b;
533
+
534
+ var aUpdated = false;
535
+
536
+ var A = React.createClass({
537
+ getInitialState: function() {
538
+ return {x: 0};
539
+ },
540
+ componentDidUpdate: function() {
541
+ expect(b.getDOMNode().textContent).toBe("B1");
542
+ aUpdated = true;
543
+ },
544
+ render: function() {
545
+ return <div>A{this.state.x}</div>;
546
+ }
547
+ });
548
+
549
+ var B = React.createClass({
550
+ getInitialState: function() {
551
+ return {x: 0};
552
+ },
553
+ render: function() {
554
+ return <div>B{this.state.x}</div>;
555
+ }
556
+ });
557
+
558
+ a = ReactTestUtils.renderIntoDocument(<A />);
559
+ b = ReactTestUtils.renderIntoDocument(<B />);
560
+
561
+ ReactUpdates.batchedUpdates(function() {
562
+ a.setState({x: 1});
563
+ b.setState({x: 1});
564
+ });
565
+
566
+ expect(aUpdated).toBe(true);
567
+ });
568
});