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

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