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