Deletion tracking
When we reconcile children we need to track the deletions that happen so that we can perf side-effects later as a result. The data structure is a linked list where the next pointer uses the nextEffect pointer. However, we need to store this side-effect list for reuse if we end up reusing the progressedChild set. That's why I add a separate first/last pointer into this list so that we can keep it for later.
Sebastian Markbage committed
Oct 4, 2016 at 01:10 UTC
e9e645a9b1a7d2081ed455ac032e9dd7e2c6a11d
2 files changed
+85
-119
src/renderers/shared/fiber/ReactChildFiber.js
+76
-119
@@ -70,8 +70,26 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
70
// Noop.
71
return;
72
}
73
-
74
- // TODO: Add this child to the side-effect queue for deletion.
73
+ if (!shouldClone) {
74
+ // When we're reconciling in place we have a work in progress copy. We
75
+ // actually want the current copy. If there is no current copy, then we
76
+ // don't need to track deletion side-effects.
77
+ if (!childToDelete.alternate) {
78
+ return;
79
+ }
80
+ childToDelete = childToDelete.alternate;
81
+ }
82
+ // Deletions are added in reversed order so we add it to the front.
83
+ const last = returnFiber.progressedLastDeletion;
84
+ if (last) {
85
+ last.nextEffect = childToDelete;
86
+ returnFiber.progressedLastDeletion = childToDelete;
87
+ } else {
88
+ returnFiber.progressedFirstDeletion =
89
+ returnFiber.progressedLastDeletion =
90
+ childToDelete;
91
+ }
92
+ childToDelete.nextEffect = null;
93
}
94
95
function deleteRemainingChildren(
@@ -82,30 +100,33 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
100
// Noop.
101
return null;
102
}
85
- // TODO: Add these children to the side-effect queue for deletion.
103
+
104
+ // TODO: For the shouldClone case, this could be micro-optimized a bit by
105
+ // assuming that after the first child we've already added everything.
106
+ let childToDelete = currentFirstChild;
107
+ while (childToDelete) {
108
+ deleteChild(returnFiber, childToDelete);
109
+ childToDelete = childToDelete.sibling;
110
+ }
111
return null;
112
}
113
89
- function mapAndDeleteRemainingChildren(
114
+ function mapRemainingChildren(
115
returnFiber : Fiber,
116
currentFirstChild : Fiber
92
- ) : Map<string, Fiber> {
117
+ ) : Map<string | number, Fiber> {
118
// Add the remaining children to a temporary map so that we can find them by
94
- // keys quickly. At the same time, we'll flag them all for deletion. However,
95
- // we will then undo the deletion as we restore children. Implicit (null) keys
96
- // don't get added to this set.
97
- const existingChildren : Map<string, Fiber> = new Map();
119
+ // keys quickly. Implicit (null) keys get added to this set with their index
120
+ // instead.
121
+ const existingChildren : Map<string | number, Fiber> = new Map();
122
+
123
let existingChild = currentFirstChild;
124
while (existingChild) {
125
if (existingChild.key !== null) {
126
existingChildren.set(existingChild.key, existingChild);
127
+ } else {
128
+ existingChildren.set(existingChild.index, existingChild);
129
}
103
- // Add everything to the delete queue
104
- // Actually... It is not possible to delete things from the queue since
105
- // we don't have access to the previous link. Does that mean we need a
106
- // second pass to add them? We should be able to keep track of the
107
- // previous deletion as we're iterating through the list the next time.
108
- // That way we know which item to patch when we delete a deletion.
130
existingChild = existingChild.sibling;
131
}
132
return existingChildren;
@@ -332,12 +353,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
353
switch (newChild.$$typeof) {
354
case REACT_ELEMENT_TYPE: {
355
if (newChild.key === key) {
335
- return updateElement(
336
- returnFiber,
337
- oldFiber,
338
- newChild,
339
- priority
340
- );
356
+ return updateElement(returnFiber, oldFiber, newChild, priority);
357
} else {
358
return null;
359
}
@@ -345,12 +361,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
361
362
case REACT_COROUTINE_TYPE: {
363
if (newChild.key === key) {
348
- return updateCoroutine(
349
- returnFiber,
350
- oldFiber,
351
- newChild,
352
- priority
353
- );
364
+ return updateCoroutine(returnFiber, oldFiber, newChild, priority);
365
} else {
366
return null;
367
}
@@ -358,12 +369,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
369
370
case REACT_YIELD_TYPE: {
371
if (newChild.key === key) {
361
- return updateYield(
362
- returnFiber,
363
- oldFiber,
364
- newChild,
365
- priority
366
- );
372
+ return updateYield(returnFiber, oldFiber, newChild, priority);
373
} else {
374
return null;
375
}
@@ -384,9 +390,9 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
390
}
391
392
function updateFromMap(
387
- existingChildren : Map<string, Fiber>,
393
+ existingChildren : Map<string | number, Fiber>,
394
returnFiber : Fiber,
389
- oldFiber : ?Fiber,
395
+ newIdx : number,
396
newChild : any,
397
priority : PriorityLevel
398
) : ?Fiber {
@@ -397,88 +403,37 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
403
if (typeof newChild === 'string' || typeof newChild === 'number') {
404
// Text nodes doesn't have keys, so we neither have to check the old nor
405
// new node for the key. If both are text nodes, they match.
400
- return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
406
+ const matchedFiber = existingChildren.get(newIdx) || null;
407
+ return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority);
408
}
409
410
if (typeof newChild === 'object' && newChild !== null) {
411
switch (newChild.$$typeof) {
412
case REACT_ELEMENT_TYPE: {
406
- if (newChild.key === null) {
407
- // For implicit keys, we'll use the existing fiber in this slot
408
- // but only if it also is an implicit key.
409
- return updateElement(
410
- returnFiber,
411
- oldFiber && oldFiber.key === null ? oldFiber : null,
412
- newChild,
413
- priority
414
- );
415
- } else {
416
- // For explicit keys we look for an existing fiber in the map.
417
- // TODO: We could test oldFiber.key first incase it happens to be
418
- // the same key but it might not be worth it given the likelihood.
419
- const matchedFiber = existingChildren.get(newChild.key);
420
- return updateElement(
421
- returnFiber,
422
- matchedFiber ? matchedFiber : null,
423
- newChild,
424
- priority
425
- );
426
- }
413
+ const matchedFiber = existingChildren.get(
414
+ newChild.key === null ? newIdx : newChild.key
415
+ ) || null;
416
+ return updateElement(returnFiber, matchedFiber, newChild, priority);
417
}
418
419
case REACT_COROUTINE_TYPE: {
430
- if (newChild.key === null) {
431
- // For implicit keys, we'll use the existing fiber in this slot
432
- // but only if it also is an implicit key.
433
- return updateCoroutine(
434
- returnFiber,
435
- oldFiber && oldFiber.key === null ? oldFiber : null,
436
- newChild,
437
- priority
438
- );
439
- } else {
440
- // For explicit keys we look for an existing fiber in the map.
441
- // TODO: We could test oldFiber.key first incase it happens to be
442
- // the same key but it might not be worth it given the likelihood.
443
- const matchedFiber = existingChildren.get(newChild.key);
444
- return updateCoroutine(
445
- returnFiber,
446
- matchedFiber ? matchedFiber : null,
447
- newChild,
448
- priority
449
- );
450
- }
420
+ const matchedFiber = existingChildren.get(
421
+ newChild.key === null ? newIdx : newChild.key
422
+ ) || null;
423
+ return updateCoroutine(returnFiber, matchedFiber, newChild, priority);
424
}
425
426
case REACT_YIELD_TYPE: {
454
- if (newChild.key === null) {
455
- // For implicit keys, we'll use the existing fiber in this slot
456
- // but only if it also is an implicit key.
457
- return updateYield(
458
- returnFiber,
459
- oldFiber && oldFiber.key === null ? oldFiber : null,
460
- newChild,
461
- priority
462
- );
463
- } else {
464
- // For explicit keys we look for an existing fiber in the map.
465
- // TODO: We could test oldFiber.key first incase it happens to be
466
- // the same key but it might not be worth it given the likelihood.
467
- const matchedFiber = existingChildren.get(newChild.key);
468
- return updateYield(
469
- returnFiber,
470
- matchedFiber ? matchedFiber : null,
471
- newChild,
472
- priority
473
- );
474
- }
427
+ const matchedFiber = existingChildren.get(
428
+ newChild.key === null ? newIdx : newChild.key
429
+ ) || null;
430
+ return updateYield(returnFiber, matchedFiber, newChild, priority);
431
}
432
}
433
434
if (isArray(newChild) || getIteratorFn(newChild)) {
479
- // Fragments doesn't have keys so if the previous is a fragment, we
480
- // update it.
481
- return updateFragment(returnFiber, oldFiber, newChild, priority);
435
+ const matchedFiber = existingChildren.get(newIdx) || null;
436
+ return updateFragment(returnFiber, matchedFiber, newChild, priority);
437
}
438
}
439
@@ -581,29 +536,30 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
536
return resultingFirstChild;
537
}
538
584
- // Mark all children as deleted and add them to a key map for quick lookups.
585
- const existingChildren = mapAndDeleteRemainingChildren(returnFiber, oldFiber);
539
+ // Add all children to a key map for quick lookups.
540
+ const existingChildren = mapRemainingChildren(returnFiber, oldFiber);
541
542
// Keep scanning and use the map to restore deleted items as moves.
543
for (; newIdx < newChildren.length; newIdx++) {
589
- // TODO: Since the mutation of existing fibers can happen at any order
590
- // we might break the link before we're done with it. :(
591
- if (oldFiber) {
592
- if (oldFiber.index > newIdx) {
593
- nextOldFiber = oldFiber;
594
- oldFiber = null;
595
- } else {
596
- nextOldFiber = oldFiber.sibling;
597
- }
598
- }
544
const newFiber = updateFromMap(
545
existingChildren,
546
returnFiber,
602
- oldFiber,
547
+ newIdx,
548
newChildren[newIdx],
549
priority
550
);
551
if (newFiber) {
552
+ if (shouldTrackSideEffects) {
553
+ if (newFiber.alternate) {
554
+ // The new fiber is a work in progress, but if there exists a
555
+ // current, that means that we reused the fiber. We need to delete
556
+ // it from the child list so that we don't add it to the deletion
557
+ // list.
558
+ existingChildren.delete(
559
+ newFiber.key === null ? newFiber.index : newFiber.key
560
+ );
561
+ }
562
+ }
563
lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx);
564
if (!previousNewFiber) {
565
resultingFirstChild = newFiber;
@@ -612,12 +568,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) {
568
}
569
previousNewFiber = newFiber;
570
}
615
- // We will keep traversing the oldFiber in order, in case the new child
616
- // has a null key that we'll need to match in the same slot.
617
- oldFiber = nextOldFiber;
571
}
572
620
- // TODO: Add deletion side-effects to the returnFiber's side-effects.
573
+ if (shouldTrackSideEffects) {
574
+ // Any existing children that we're consumed above were deleted. We need
575
+ // to add them to the deletion list.
576
+ existingChildren.forEach(child => deleteChild(returnFiber, child));
577
+ }
578
579
return resultingFirstChild;
580
}
src/renderers/shared/fiber/ReactFiber.js
+9
@@ -114,6 +114,13 @@ export type Fiber = Instance & {
114
// or may not be the same as the "current" child.
115
progressedChild: ?Fiber,
116
117
+ // When we reconcile children onto progressedChild it is possible that we have
118
+ // to delete some child fibers. We need to keep track of this side-effects so
119
+ // that if we continue later on, we have to include those effects. Deletions
120
+ // are added in the reverse order from sibling pointers.
121
+ progressedFirstDeletion: ?Fiber,
122
+ progressedLastDeletion: ?Fiber,
123
+
124
// This is a pooled version of a Fiber. Every fiber that gets updated will
125
// eventually have a pair. There are cases when we can clean up pairs to save
126
// memory if we need to.
@@ -175,6 +182,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
182
pendingWorkPriority: NoWork,
183
progressedPriority: NoWork,
184
progressedChild: null,
185
+ progressedFirstDeletion: null,
186
+ progressedLastDeletion: null,
187
188
alternate: null,
189