@samitouri / QOS-React-2 / commits / ddf53f97f0

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 });