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

Use Singleton LifeCycle State

All entry points are for reconciliation are within batching strategies. Since we don't have any batching strategies with synchronous updates, there can't be more than one life-cycle method on the stack at any given time. Therefore, it's safe to move the composite life cycle flag to a singleton. This saves us some memory management. I think that we can get rid of these life cycle states completely in the future.

Sebastian Markbage committed Jan 27, 2015 at 01:19 UTC bebc568ceb6d93dd5ded624842f9b7e9d9a84e8e
5 files changed +115 -107
src/classic/class/ReactClass.js
+5 -3
@@ -15,6 +15,7 @@ var ReactComponentBase = require('ReactComponentBase');
15 var ReactElement = require('ReactElement');
16 var ReactErrorUtils = require('ReactErrorUtils');
17 var ReactInstanceMap = require('ReactInstanceMap');
18 +var ReactLifeCycle = require('ReactLifeCycle');
19 var ReactPropTypeLocations = require('ReactPropTypeLocations');
20 var ReactPropTypeLocationNames = require('ReactPropTypeLocationNames');
21 var ReactUpdateQueue = require('ReactUpdateQueue');
@@ -734,9 +735,10 @@ var ReactClassMixin = {
735 */
736 isMounted: function() {
737 var internalInstance = ReactInstanceMap.get(this);
737 - // In theory, isMounted is always true if it exists in the map.
738 - // TODO: Remove the internal isMounted method.
739 - return internalInstance && internalInstance.isMounted();
738 + return (
739 + internalInstance &&
740 + internalInstance !== ReactLifeCycle.currentlyMountingInstance
741 + );
742 },
743
744 /**
src/core/ReactCompositeComponent.js
+47 -31
@@ -42,6 +42,33 @@ function getDeclarationErrorAddendum(component) {
42 return '';
43 }
44
45 +/**
46 + * ------------------ The Life-Cycle of a Composite Component ------------------
47 + *
48 + * - constructor: Initialization of state. The instance is now retained.
49 + * - componentWillMount
50 + * - render
51 + * - [children's constructors]
52 + * - [children's componentWillMount and render]
53 + * - [children's componentDidMount]
54 + * - componentDidMount
55 + *
56 + * Update Phases:
57 + * - componentWillReceiveProps (only called if parent updated)
58 + * - shouldComponentUpdate
59 + * - componentWillUpdate
60 + * - render
61 + * - [children's constructors or receive props phases]
62 + * - componentDidUpdate
63 + *
64 + * - componentWillUnmount
65 + * - [children's componentWillUnmount]
66 + * - [children destroyed]
67 + * - (destroyed): The instance is now blank, released by React and ready for GC.
68 + *
69 + * -----------------------------------------------------------------------------
70 + */
71 +
72 /**
73 * An incrementing ID assigned to each component when it is mounted. This is
74 * used to enforce the order in which `ReactUpdates` updates dirty components.
@@ -71,7 +98,6 @@ var ReactCompositeComponentMixin = {
98 this._pendingElement = null;
99 this._pendingState = null;
100 this._pendingForceUpdate = false;
74 - this._compositeLifeCycleState = null;
101
102 this._renderedComponent = null;
103
@@ -83,16 +109,6 @@ var ReactCompositeComponentMixin = {
109 this._pendingCallbacks = null;
110 },
111
86 - /**
87 - * Checks whether or not this composite component is mounted.
88 - * @return {boolean} True if mounted, false otherwise.
89 - * @protected
90 - * @final
91 - */
92 - isMounted: function() {
93 - return this._compositeLifeCycleState !== ReactLifeCycle.MOUNTING;
94 - },
95 -
112 /**
113 * Initializes the component, renders markup, and registers event listeners.
114 *
@@ -127,8 +143,6 @@ var ReactCompositeComponentMixin = {
143 // Store a reference from the instance back to the internal representation
144 ReactInstanceMap.set(inst, this);
145
130 - this._compositeLifeCycleState = ReactLifeCycle.MOUNTING;
131 -
146 if (__DEV__) {
147 this._warnIfContextsDiffer(this._currentElement._context, context);
148 }
@@ -176,13 +190,18 @@ var ReactCompositeComponentMixin = {
190 '%s.state: must be set to an object or null',
191 this.getName() || 'ReactCompositeComponent'
192 );
179 - inst.state = initialState;
193
194 this._pendingState = null;
195 this._pendingForceUpdate = false;
196
197 if (inst.componentWillMount) {
185 - inst.componentWillMount();
198 + var previouslyMounting = ReactLifeCycle.currentlyMountingInstance;
199 + ReactLifeCycle.currentlyMountingInstance = this;
200 + try {
201 + inst.componentWillMount();
202 + } finally {
203 + ReactLifeCycle.currentlyMountingInstance = previouslyMounting;
204 + }
205 // When mounting, calls to `setState` by `componentWillMount` will set
206 // `this._pendingState` without triggering a re-render.
207 if (this._pendingState) {
@@ -197,8 +216,6 @@ var ReactCompositeComponentMixin = {
216 this._currentElement.type // The wrapping type
217 );
218
200 - // Done with mounting, `setState` will now trigger UI changes.
201 - this._compositeLifeCycleState = null;
219 var markup = ReactReconciler.mountComponent(
220 this._renderedComponent,
221 rootID,
@@ -221,11 +238,15 @@ var ReactCompositeComponentMixin = {
238 unmountComponent: function() {
239 var inst = this._instance;
240
224 - this._compositeLifeCycleState = ReactLifeCycle.UNMOUNTING;
241 if (inst.componentWillUnmount) {
226 - inst.componentWillUnmount();
242 + var previouslyUnmounting = ReactLifeCycle.currentlyUnmountingInstance;
243 + ReactLifeCycle.currentlyUnmountingInstance = this;
244 + try {
245 + inst.componentWillUnmount();
246 + } finally {
247 + ReactLifeCycle.currentlyUnmountingInstance = previouslyUnmounting;
248 + }
249 }
228 - this._compositeLifeCycleState = null;
250
251 ReactReconciler.unmountComponent(this._renderedComponent);
252 this._renderedComponent = null;
@@ -238,6 +259,8 @@ var ReactCompositeComponentMixin = {
259
260 ReactComponentEnvironment.unmountIDFromEnvironment(this._rootNodeID);
261
262 + // These fields do not really need to be reset since this object is no
263 + // longer accessible.
264 this._context = null;
265 this._rootNodeID = null;
266
@@ -432,14 +455,6 @@ var ReactCompositeComponentMixin = {
455 },
456
457 receiveComponent: function(nextElement, transaction, nextContext) {
435 - var compositeLifeCycleState = this._compositeLifeCycleState;
436 - // Do not trigger a state transition if we are in the middle of mounting or
437 - // receiving props because both of those will already be doing this.
438 - if (compositeLifeCycleState === ReactLifeCycle.MOUNTING ||
439 - compositeLifeCycleState === ReactLifeCycle.RECEIVING_PROPS) {
440 - return;
441 - }
442 -
458 var prevElement = this._currentElement;
459 var prevContext = this._context;
460
@@ -552,14 +567,15 @@ var ReactCompositeComponentMixin = {
567 }
568 }
569
555 - this._compositeLifeCycleState = ReactLifeCycle.RECEIVING_PROPS;
570 + // An update here will schedule an update but immediately set
571 + // _pendingState which will ensure that any state updates gets
572 + // immediately reconciled instead of waiting for the next batch.
573 +
574 if (inst.componentWillReceiveProps) {
575 inst.componentWillReceiveProps(nextProps, nextContext);
576 }
577 }
578
561 - this._compositeLifeCycleState = null;
562 -
579 var nextState = this._pendingState || inst.state;
580 this._pendingState = null;
581
src/core/ReactLifeCycle.js
+16 -41
@@ -11,50 +11,25 @@
11
12 "use strict";
13
14 -var keyMirror = require('keyMirror');
15 -
14 /**
17 - * `ReactCompositeComponent` maintains an auxiliary life cycle state in
18 - * `this._compositeLifeCycleState` (which can be null).
19 - *
20 - * This is different from the life cycle state maintained by `ReactComponent`.
21 - * The following diagram shows how the states overlap in
22 - * time. There are times when the CompositeLifeCycle is null - at those times it
23 - * is only meaningful to look at ComponentLifeCycle alone.
15 + * This module manages the bookkeeping when a component is in the process
16 + * of being mounted or being unmounted. This is used as a way to enforce
17 + * invariants (or warnings) when it is not recommended to call
18 + * setState/forceUpdate.
19 *
25 - * Top Row: ReactComponent.ComponentLifeCycle
26 - * Low Row: ReactComponent.CompositeLifeCycle
20 + * currentlyMountingInstance: During the construction phase, it is not possible
21 + * to trigger an update since the instance is not fully mounted yet. However, we
22 + * currently allow this as a convenience for mutating the initial state.
23 *
28 - * +-------+---------------------------------+--------+
29 - * | UN | MOUNTED | UN |
30 - * |MOUNTED| | MOUNTED|
31 - * +-------+---------------------------------+--------+
32 - * | ^--------+ +-------+ +--------^ |
33 - * | | | | | | | |
34 - * | 0--|MOUNTING|-0-|RECEIVE|-0-| UN |--->0 |
35 - * | | | |PROPS | |MOUNTING| |
36 - * | | | | | | | |
37 - * | | | | | | | |
38 - * | +--------+ +-------+ +--------+ |
39 - * | | | |
40 - * +-------+---------------------------------+--------+
24 + * currentlyUnmountingInstance: During the unmounting phase, the instance is
25 + * still mounted and can therefore schedule an update. However, this is not
26 + * recommended and probably an error since it's about to be unmounted.
27 + * Therefore we still want to trigger in an error for that case.
28 */
42 -var ReactLifeCycle = keyMirror({
43 - /**
44 - * Components in the process of being mounted respond to state changes
45 - * differently.
46 - */
47 - MOUNTING: null,
48 - /**
49 - * Components in the process of being unmounted are guarded against state
50 - * changes.
51 - */
52 - UNMOUNTING: null,
53 - /**
54 - * Components that are mounted and receiving new props respond to state
55 - * changes differently.
56 - */
57 - RECEIVING_PROPS: null
58 -});
29 +
30 +var ReactLifeCycle = {
31 + currentlyMountingInstance: null,
32 + currentlyUnmountingInstance: null
33 +};
34
35 module.exports = ReactLifeCycle;
src/core/ReactUpdateQueue.js
+5 -5
@@ -21,7 +21,7 @@ var assign = require('Object.assign');
21 var invariant = require('invariant');
22
23 function enqueueUpdate(internalInstance) {
24 - if (internalInstance._compositeLifeCycleState !== ReactLifeCycle.MOUNTING) {
24 + if (internalInstance !== ReactLifeCycle.currentlyMountingInstance) {
25 // If we're in a componentWillMount handler, don't enqueue a rerender
26 // because ReactUpdates assumes we're in a browser context (which is
27 // wrong for server rendering) and we're about to do a render anyway.
@@ -49,8 +49,7 @@ function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
49 callerName
50 );
51 invariant(
52 - internalInstance._compositeLifeCycleState !==
53 - ReactLifeCycle.UNMOUNTING,
52 + internalInstance !== ReactLifeCycle.currentlyUnmountingInstance,
53 '%s(...): Cannot call %s() on an unmounting component.',
54 callerName,
55 callerName
@@ -84,7 +83,7 @@ var ReactUpdateQueue = {
83 internalInstance,
84 'Cannot enqueue a callback on an instance that is unmounted.'
85 );
87 - if (internalInstance._compositeLifeCycleState === ReactLifeCycle.MOUNTING) {
86 + if (internalInstance === ReactLifeCycle.currentlyMountingInstance) {
87 // Ignore callbacks in componentWillMount. See enqueueUpdate.
88 return;
89 }
@@ -95,7 +94,8 @@ var ReactUpdateQueue = {
94 }
95 // TODO: The callback here is ignored when setState is called from
96 // componentWillMount. Either fix it or disallow doing so completely in
98 - // favor of getInitialState.
97 + // favor of getInitialState. Alternatively, we can disallow
98 + // componentWillMount during server-side rendering.
99 enqueueUpdate(internalInstance);
100 },
101
src/core/__tests__/ReactComponentLifeCycle-test.js
+42 -27
@@ -14,13 +14,9 @@
14 var keyMirror = require('keyMirror');
15
16 var React;
17 -var ReactTestUtils;
18 -var ReactCompositeComponent;
17 +var ReactLifeCycle;
18 var ReactInstanceMap;
20 -var CompositeComponentLifeCycle;
21 -
22 -var getCompositeLifeCycle;
23 -var getLifeCycleState;
19 +var ReactTestUtils;
20
21 var clone = function(o) {
22 return JSON.parse(JSON.stringify(o));
@@ -84,6 +80,44 @@ var ComponentLifeCycle = keyMirror({
80 UNMOUNTED: null
81 });
82
83 +/**
84 + * Composite components can also be in one of these life cycles.
85 + */
86 +var CompositeComponentLifeCycle = keyMirror({
87 + /**
88 + * Mounted components have a DOM node representation and are capable of
89 + * receiving new props.
90 + */
91 + MOUNTING: null,
92 + /**
93 + * Unmounted components are inactive and cannot receive new props.
94 + */
95 + UNMOUNTING: null
96 +});
97 +
98 +function getCompositeLifeCycle(instance) {
99 + var internalInstance = ReactInstanceMap.get(instance);
100 + if (!internalInstance) {
101 + return null;
102 + }
103 + if (ReactLifeCycle.currentlyMountingInstance === internalInstance) {
104 + return CompositeComponentLifeCycle.MOUNTING;
105 + }
106 + if (ReactLifeCycle.currentlyUnmountingInstance === internalInstance) {
107 + return CompositeComponentLifeCycle.UNMOUNTING;
108 + }
109 + return null;
110 +}
111 +
112 +function getLifeCycleState(instance) {
113 + var internalInstance = ReactInstanceMap.get(instance);
114 + // Once a component gets mounted, it has an internal instance, once it
115 + // gets unmounted, it loses that internal instance.
116 + return internalInstance ?
117 + ComponentLifeCycle.MOUNTED :
118 + ComponentLifeCycle.UNMOUNTED;
119 +}
120 +
121 /**
122 * TODO: We should make any setState calls fail in
123 * `getInitialState` and `componentWillMount`. They will usually fail
@@ -96,27 +130,8 @@ describe('ReactComponentLifeCycle', function() {
130 require('mock-modules').dumpCache();
131 React = require('React');
132 ReactTestUtils = require('ReactTestUtils');
99 - ReactCompositeComponent = require('ReactCompositeComponent');
100 - CompositeComponentLifeCycle = require('ReactLifeCycle');
101 -
133 + ReactLifeCycle = require('ReactLifeCycle');
134 ReactInstanceMap = require('ReactInstanceMap');
103 -
104 - getCompositeLifeCycle = function(instance) {
105 - var internalInstance = ReactInstanceMap.get(instance);
106 - if (!internalInstance) {
107 - return null;
108 - }
109 - return internalInstance._compositeLifeCycleState;
110 - };
111 -
112 - getLifeCycleState = function(instance) {
113 - var internalInstance = ReactInstanceMap.get(instance);
114 - // Once a component gets mounted, it has an internal instance, once it
115 - // gets unmounted, it loses that internal instance.
116 - return internalInstance ?
117 - ComponentLifeCycle.MOUNTED :
118 - ComponentLifeCycle.UNMOUNTED;
119 - };
135 });
136
137 it('should not reuse an instance when it has been unmounted', function() {
@@ -345,7 +360,7 @@ describe('ReactComponentLifeCycle', function() {
360 ComponentLifeCycle.MOUNTED
361 );
362 expect(instance._testJournal.compositeLifeCycleInInitialRender).toBe(
348 - CompositeComponentLifeCycle.MOUNTING
363 + null
364 );
365
366 expect(getLifeCycleState(instance)).toBe(ComponentLifeCycle.MOUNTED);