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

Add more life-cycles to Fiber

This refactors the initialization process so that we can share it with the "module pattern" initialization. There are a few new interesting scenarios unlocked by this. E.g. constructor -> componentWillMount -> shouldComponentUpdate -> componentDidMount when a render is aborted and resumed. If shouldComponentUpdate returns true then we create a new instance instead of trying componentWillMount again or componentWillReceiveProps without being mounted. Another strange thing is that the "previous props and state" during componentWillReceiveProps, shouldComponentUpdate and componentWillUpdate are all the previous render attempt. However, componentDidMount's previous is the props/state at the previous commit. That is because the first three can execute multiple times before a didMount.

Sebastian Markbage committed Oct 19, 2016 at 02:06 UTC a87ec42f2ad5cb996cfc92e7023372e667931172
2 files changed +171 -62
src/renderers/shared/fiber/ReactFiberBeginWork.js
+26 -53
@@ -40,9 +40,6 @@ var {
40 NoWork,
41 OffscreenPriority,
42 } = require('ReactPriorityLevel');
43 -var {
44 - mergeUpdateQueue,
45 -} = require('ReactFiberUpdateQueue');
43 var {
44 Placement,
45 } = require('ReactTypeOfSideEffect');
@@ -51,7 +48,11 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent');
48 module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) {
49
50 const {
54 - mount,
51 + adoptClassInstance,
52 + constructClassInstance,
53 + mountClassInstance,
54 + resumeMountClassInstance,
55 + updateClassInstance,
56 } = ReactFiberClassComponent(scheduleUpdate);
57
58 function markChildAsProgressed(current, workInProgress, priorityLevel) {
@@ -156,54 +157,27 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
157 }
158
159 function updateClassComponent(current : ?Fiber, workInProgress : Fiber) {
159 - // A class component update is the result of either new props or new state.
160 - // Account for the possibly of missing pending props by falling back to the
161 - // memoized props.
162 - var props = workInProgress.pendingProps;
163 - if (!props) {
164 - // If there isn't any new props, then we'll reuse the memoized props.
165 - // This could be from already completed work.
166 - props = workInProgress.memoizedProps;
167 - if (!props) {
168 - throw new Error('There should always be pending or memoized props.');
160 + let shouldUpdate;
161 + if (!current) {
162 + if (!workInProgress.stateNode) {
163 + // In the initial pass we might need to construct the instance.
164 + constructClassInstance(workInProgress);
165 + mountClassInstance(workInProgress);
166 + shouldUpdate = true;
167 + } else {
168 + // In a resume, we'll already have an instance we can reuse.
169 + shouldUpdate = resumeMountClassInstance(workInProgress);
170 }
170 - }
171 -
172 - // Compute the state using the memoized state and the update queue.
173 - var updateQueue = workInProgress.updateQueue;
174 - var previousState = workInProgress.memoizedState;
175 - var state;
176 - if (updateQueue) {
177 - state = mergeUpdateQueue(updateQueue, previousState, props);
171 } else {
179 - state = previousState;
172 + shouldUpdate = updateClassInstance(current, workInProgress);
173 }
181 -
182 - var instance = workInProgress.stateNode;
183 - if (!instance) {
184 - var ctor = workInProgress.type;
185 - workInProgress.stateNode = instance = new ctor(props);
186 - mount(workInProgress, instance);
187 - state = instance.state || null;
188 - } else if (typeof instance.shouldComponentUpdate === 'function' &&
189 - !(updateQueue && updateQueue.isForced)) {
190 - if (workInProgress.memoizedProps !== null) {
191 - // Reset the props, in case this is a ping-pong case rather than a
192 - // completed update case. For the completed update case, the instance
193 - // props will already be the memoizedProps.
194 - instance.props = workInProgress.memoizedProps;
195 - instance.state = workInProgress.memoizedState;
196 - if (!instance.shouldComponentUpdate(props, state)) {
197 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
198 - }
199 - }
174 + if (!shouldUpdate) {
175 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
176 }
201 -
202 - instance.props = props;
203 - instance.state = state;
204 - var nextChildren = instance.render();
177 + // Rerender
178 + const instance = workInProgress.stateNode;
179 + const nextChildren = instance.render();
180 reconcileChildren(current, workInProgress, nextChildren);
206 -
181 return workInProgress.child;
182 }
183
@@ -258,22 +232,21 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, s
232 }
233
234 function mountIndeterminateComponent(current, workInProgress) {
235 + if (current) {
236 + throw new Error('An indeterminate component should never have mounted.');
237 + }
238 var fn = workInProgress.type;
239 var props = workInProgress.pendingProps;
240 var value = fn(props);
241 if (typeof value === 'object' && value && typeof value.render === 'function') {
242 // Proceed under the assumption that this is a class instance
243 workInProgress.tag = ClassComponent;
267 - if (current) {
268 - current.tag = ClassComponent;
269 - }
244 + adoptClassInstance(workInProgress, value);
245 + mountClassInstance(workInProgress);
246 value = value.render();
247 } else {
248 // Proceed under the assumption that this is a functional component
249 workInProgress.tag = FunctionalComponent;
274 - if (current) {
275 - current.tag = FunctionalComponent;
276 - }
250 }
251 reconcileChildren(current, workInProgress, value);
252 return workInProgress.child;
src/renderers/shared/fiber/ReactFiberClassComponent.js
+145 -9
@@ -21,6 +21,7 @@ var {
21 createUpdateQueue,
22 addToQueue,
23 addCallbackToQueue,
24 + mergeUpdateQueue,
25 } = require('ReactFiberUpdateQueue');
26 var ReactInstanceMap = require('ReactInstanceMap');
27
@@ -70,20 +71,155 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori
71 },
72 };
73
73 - function mount(workInProgress : Fiber, instance : any) {
74 - const state = instance.state || null;
75 - // The initial state must be added to the update queue in case
76 - // setState is called before the initial render.
77 - if (state !== null) {
78 - workInProgress.updateQueue = createUpdateQueue(state);
79 - }
74 + function adoptClassInstance(workInProgress : Fiber, instance : any) : void {
75 + instance.updater = updater;
76 + workInProgress.stateNode = instance;
77 // The instance needs access to the fiber so that it can schedule updates
78 ReactInstanceMap.set(instance, workInProgress);
82 - instance.updater = updater;
79 + }
80 +
81 + function constructClassInstance(workInProgress : Fiber) : any {
82 + const ctor = workInProgress.type;
83 + const props = workInProgress.pendingProps;
84 + const instance = new ctor(props);
85 + adoptClassInstance(workInProgress, instance);
86 + return instance;
87 + }
88 +
89 + // Invokes the mount life-cycles on a previously never rendered instance.
90 + function mountClassInstance(workInProgress : Fiber) : void {
91 + const instance = workInProgress.stateNode;
92 +
93 + const state = instance.state || null;
94 +
95 + // A class component update is the result of either new props or new state.
96 + // Account for the possibly of missing pending props by falling back to the
97 + // memoized props.
98 + let props = workInProgress.pendingProps;
99 + if (!props) {
100 + throw new Error('There must be pending props for an initial mount.');
101 + }
102 +
103 + instance.props = props;
104 + instance.state = state;
105 +
106 + if (typeof instance.componentWillMount === 'function') {
107 + instance.componentWillMount();
108 + // If we had additional state updates during this life-cycle, let's
109 + // process them now.
110 + const updateQueue = workInProgress.updateQueue;
111 + if (updateQueue) {
112 + instance.state = mergeUpdateQueue(updateQueue, state, props);
113 + }
114 + }
115 + }
116 +
117 + // Called on a preexisting class instance. Returns false if a resumed render
118 + // could be reused.
119 + function resumeMountClassInstance(workInProgress : Fiber) : boolean {
120 + const instance = workInProgress.stateNode;
121 + let newState = workInProgress.memoizedState;
122 + let newProps = workInProgress.pendingProps;
123 + if (!newProps) {
124 + // If there isn't any new props, then we'll reuse the memoized props.
125 + // This could be from already completed work.
126 + newProps = workInProgress.memoizedProps;
127 + if (!newProps) {
128 + throw new Error('There should always be pending or memoized props.');
129 + }
130 + }
131 +
132 + // TODO: Should we deal with a setState that happened after the last
133 + // componentWillMount and before this componentWillMount? Probably
134 + // unsupported anyway.
135 +
136 + const updateQueue = workInProgress.updateQueue;
137 +
138 + // If this completed, we might be able to just reuse this instance.
139 + if (typeof instance.shouldComponentUpdate === 'function' &&
140 + !(updateQueue && updateQueue.isForced) &&
141 + workInProgress.memoizedProps !== null &&
142 + !instance.shouldComponentUpdate(newProps, newState)) {
143 + return false;
144 + }
145 +
146 + // If we didn't bail out we need to construct a new instance. We don't
147 + // want to reuse one that failed to fully mount.
148 + const newInstance = constructClassInstance(workInProgress);
149 + newInstance.props = newProps;
150 + newInstance.state = newState = newInstance.state || null;
151 +
152 + if (typeof newInstance.componentWillMount === 'function') {
153 + newInstance.componentWillMount();
154 + // If we had additional state updates during this life-cycle, let's
155 + // process them now.
156 + const newUpdateQueue = workInProgress.updateQueue;
157 + if (newUpdateQueue) {
158 + instance.state = mergeUpdateQueue(newUpdateQueue, newState, newProps);
159 + }
160 + }
161 + return true;
162 + }
163 +
164 + // Invokes the update life-cycles and returns false if it shouldn't rerender.
165 + function updateClassInstance(current : Fiber, workInProgress : Fiber) : boolean {
166 + const instance = workInProgress.stateNode;
167 +
168 + const oldProps = current.memoizedProps;
169 + let newProps = workInProgress.pendingProps;
170 + if (!newProps) {
171 + // If there aren't any new props, then we'll reuse the memoized props.
172 + // This could be from already completed work.
173 + newProps = workInProgress.memoizedProps;
174 + if (!newProps) {
175 + throw new Error('There should always be pending or memoized props.');
176 + }
177 + }
178 +
179 + // Note: During these life-cycles, instance.props/instance.state are what
180 + // ever the previously attempted to render - not the "current". However,
181 + // during componentDidUpdate we pass the "current" props.
182 +
183 + if (oldProps !== newProps) {
184 + if (typeof instance.componentWillReceiveProps === 'function') {
185 + instance.componentWillReceiveProps(newProps);
186 + }
187 + }
188 +
189 + // Compute the next state using the memoized state and the update queue.
190 + const updateQueue = workInProgress.updateQueue;
191 + const previousState = workInProgress.memoizedState;
192 + // TODO: Previous state can be null.
193 + let newState;
194 + if (updateQueue) {
195 + newState = mergeUpdateQueue(updateQueue, previousState, newProps);
196 + } else {
197 + newState = previousState;
198 + }
199 +
200 + if (typeof instance.shouldComponentUpdate === 'function' &&
201 + !(updateQueue && updateQueue.isForced) &&
202 + workInProgress.memoizedProps !== null &&
203 + !instance.shouldComponentUpdate(newProps, newState)) {
204 + // TODO: Should this get the new props/state updated regardless?
205 + return false;
206 + }
207 +
208 + if (typeof instance.componentWillUpdate === 'function') {
209 + instance.componentWillUpdate(newProps, newState);
210 + }
211 +
212 + instance.props = newProps;
213 + instance.state = newState;
214 + return true;
215 }
216
217 return {
86 - mount,
218 + adoptClassInstance,
219 + constructClassInstance,
220 + mountClassInstance,
221 + resumeMountClassInstance,
222 + updateClassInstance,
223 };
224
225 };