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

Invoke all null ref calls before any new ref calls

This reorganizes the two commit passes so that all host environment mutations happens before any life-cycles. That means that the tree is guaranteed to be in a consistent state at that point so that you can read layout etc. This also lets us to detach all refs in the same pass so that when they get invoked with new instances, that happens after it has been reset.

Sebastian Markbage committed Oct 7, 2016 at 15:46 UTC e59e5b280f4b433d6c8cd7821fa1f64ad7847e35
3 files changed +76 -45
src/renderers/shared/fiber/ReactFiberCommitWork.js
+49 -28
@@ -47,18 +47,18 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
47 }
48 }
49
50 - function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) {
51 - const ref = finishedWork.ref;
50 + function detachRefIfNeeded(current : ?Fiber, finishedWork : Fiber) {
51 if (current) {
52 const currentRef = current.ref;
54 - if (currentRef && currentRef !== ref) {
55 - // TODO: This needs to be done in a separate pass before any other refs
56 - // gets resolved. Otherwise we might invoke them in the wrong order
57 - // when the same ref switches between two components.
53 + if (currentRef && currentRef !== finishedWork.ref) {
54 currentRef(null);
55 }
56 }
61 - if (ref) {
57 + }
58 +
59 + function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) {
60 + const ref = finishedWork.ref;
61 + if (ref && (!current || current.ref !== ref)) {
62 ref(instance);
63 }
64 }
@@ -239,6 +239,46 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
239 }
240
241 function commitWork(current : ?Fiber, finishedWork : Fiber) : void {
242 + switch (finishedWork.tag) {
243 + case ClassComponent: {
244 + detachRefIfNeeded(current, finishedWork);
245 + return;
246 + }
247 + case HostContainer: {
248 + // TODO: Attach children to root container.
249 + const children = finishedWork.output;
250 + const root : FiberRoot = finishedWork.stateNode;
251 + const containerInfo : C = root.containerInfo;
252 + updateContainer(containerInfo, children);
253 + return;
254 + }
255 + case HostComponent: {
256 + const instance : I = finishedWork.stateNode;
257 + if (instance != null && current) {
258 + // Commit the work prepared earlier.
259 + const newProps = finishedWork.memoizedProps;
260 + const oldProps = current.memoizedProps;
261 + commitUpdate(instance, oldProps, newProps);
262 + }
263 + detachRefIfNeeded(current, finishedWork);
264 + return;
265 + }
266 + case HostText: {
267 + if (finishedWork.stateNode == null || !current) {
268 + throw new Error('This should only be done during updates.');
269 + }
270 + const textInstance : TI = finishedWork.stateNode;
271 + const newText : string = finishedWork.memoizedProps;
272 + const oldText : string = current.memoizedProps;
273 + commitTextUpdate(textInstance, oldText, newText);
274 + return;
275 + }
276 + default:
277 + throw new Error('This unit of work tag should not have side-effects.');
278 + }
279 + }
280 +
281 + function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void {
282 switch (finishedWork.tag) {
283 case ClassComponent: {
284 const instance = finishedWork.stateNode;
@@ -270,33 +310,13 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
310 attachRef(current, finishedWork, instance);
311 return;
312 }
273 - case HostContainer: {
274 - // TODO: Attach children to root container.
275 - const children = finishedWork.output;
276 - const root : FiberRoot = finishedWork.stateNode;
277 - const containerInfo : C = root.containerInfo;
278 - updateContainer(containerInfo, children);
279 - return;
280 - }
313 case HostComponent: {
314 const instance : I = finishedWork.stateNode;
283 - if (instance != null && current) {
284 - // Commit the work prepared earlier.
285 - const newProps = finishedWork.memoizedProps;
286 - const oldProps = current.memoizedProps;
287 - commitUpdate(instance, oldProps, newProps);
288 - }
315 attachRef(current, finishedWork, instance);
316 return;
317 }
318 case HostText: {
293 - if (finishedWork.stateNode == null || !current) {
294 - throw new Error('This should only be done during updates.');
295 - }
296 - const textInstance : TI = finishedWork.stateNode;
297 - const newText : string = finishedWork.memoizedProps;
298 - const oldText : string = current.memoizedProps;
299 - commitTextUpdate(textInstance, oldText, newText);
319 + // We have no life-cycles associated with text.
320 return;
321 }
322 default:
@@ -308,6 +328,7 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
328 commitInsertion,
329 commitDeletion,
330 commitWork,
331 + commitLifeCycles,
332 };
333
334 };
src/renderers/shared/fiber/ReactFiberScheduler.js
+24 -11
@@ -54,7 +54,8 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
54
55 const { beginWork } = ReactFiberBeginWork(config, getScheduler);
56 const { completeWork } = ReactFiberCompleteWork(config);
57 - const { commitInsertion, commitDeletion, commitWork } = ReactFiberCommitWork(config);
57 + const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } =
58 + ReactFiberCommitWork(config);
59
60 const scheduleAnimationCallback = config.scheduleAnimationCallback;
61 const scheduleDeferredCallback = config.scheduleDeferredCallback;
@@ -112,31 +113,43 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>) {
113 // Commit all the side-effects within a tree.
114 // TODO: Error handling.
115
115 - // First, we'll perform all the host insertion and deletion effects.
116 + // First, we'll perform all the host insertions, updates, deletions and
117 + // ref unmounts.
118 let effectfulFiber = finishedWork.firstEffect;
119 while (effectfulFiber) {
120 switch (effectfulFiber.effectTag) {
119 - // TODO: Should we commit host updates here? Otherwise update to parent
120 - // host components are not visible in life-cycles. Such as when you read
121 - // layout information.
122 - case Placement:
123 - case PlacementAndUpdate:
121 + case Placement: {
122 commitInsertion(effectfulFiber);
123 break;
126 - case Deletion:
124 + }
125 + case PlacementAndUpdate: {
126 + commitInsertion(effectfulFiber);
127 + const current = effectfulFiber.alternate;
128 + commitWork(current, effectfulFiber);
129 + break;
130 + }
131 + case Update: {
132 + const current = effectfulFiber.alternate;
133 + commitWork(current, effectfulFiber);
134 + break;
135 + }
136 + case Deletion: {
137 commitDeletion(effectfulFiber);
138 break;
139 + }
140 }
141 effectfulFiber = effectfulFiber.nextEffect;
142 }
143
133 - // Next, we'll perform all other effects.
144 + // Next, we'll perform all life-cycles and ref callbacks. Life-cycles
145 + // happens as a separate pass so that all effects in the entire tree have
146 + // already been invoked.
147 effectfulFiber = finishedWork.firstEffect;
148 while (effectfulFiber) {
136 - const current = effectfulFiber.alternate;
149 if (effectfulFiber.effectTag === Update ||
150 effectfulFiber.effectTag === PlacementAndUpdate) {
139 - commitWork(current, effectfulFiber);
151 + const current = effectfulFiber.alternate;
152 + commitLifeCycles(current, effectfulFiber);
153 }
154 const next = effectfulFiber.nextEffect;
155 // Ensure that we clean these up so that we don't accidentally keep them.
src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js
+3 -6
@@ -680,14 +680,11 @@ describe('ReactIncrementalSideEffects', () => {
680 ReactNoop.render(<Foo show={true} />);
681 ReactNoop.flush();
682 expect(ops).toEqual([
683 - // TODO: All detach should happen first. Currently they're interleaved.
684 - // detach
683 + // detach all refs that switched handlers first.
684 null,
686 - // reattach
687 - classInstance,
688 - // detach
685 null,
690 - // reattach
686 + // reattach as a separate phase
687 + classInstance,
688 div(),
689 ]);
690