Fiber child reconciliation

This implements the first step to proper child reconciliation.
It doesn't yet track side-effects like insert/move/delete but has
the main reconciliation algorithm in place.

The goal of this algorithm is to branch early and avoid rechecking those conditions. That leads to some duplications of code.

There are three major branches:

- Reconciling a single child per type.
- Reconciling all children that are in the same slot as before from the beginning.
- Adding remaining children to a temporary Map and reconciling them by scanning the map.

Even when we use the Map strategy we have to scan the linked list to line up "same slot" positions because React, unlike Inferno, can have implicit keys interleaved with explicit keys.
This commit is contained in:
Sebastian Markbage 2016-09-19 20:53:31 -07:00 committed by Sebastian Markbåge
parent 13f01be64f
commit edaf08fcfe
3 changed files with 683 additions and 180 deletions

View File

@ -16,8 +16,6 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine';
import type { Fiber } from 'ReactFiber';
import type { PriorityLevel } from 'ReactPriorityLevel';
import type { ReactNodeList } from 'ReactTypes';
var REACT_ELEMENT_TYPE = require('ReactElementSymbol');
var {
REACT_COROUTINE_TYPE,
@ -25,7 +23,11 @@ var {
} = require('ReactCoroutine');
var ReactFiber = require('ReactFiber');
var ReactPriorityLevel = require('ReactPriorityLevel');
var ReactReifiedYield = require('ReactReifiedYield');
var ReactTypeOfWork = require('ReactTypeOfWork');
var getIteratorFn = require('getIteratorFn');
const {
cloneFiber,
@ -38,211 +40,683 @@ const {
const {
createReifiedYield,
createUpdatedReifiedYield,
} = ReactReifiedYield;
const isArray = Array.isArray;
function ChildReconciler(shouldClone) {
const {
HostText,
CoroutineComponent,
YieldComponent,
Fragment,
} = ReactTypeOfWork;
function createSubsequentChild(
const {
NoWork,
} = ReactPriorityLevel;
function deleteChild(
returnFiber : Fiber,
childToDelete : Fiber
) {
// TODO: Add this child to the side-effect queue for deletion.
}
function deleteRemainingChildren(
returnFiber : Fiber,
currentFirstChild : ?Fiber
) {
// TODO: Add these children to the side-effect queue for deletion.
return null;
}
function mapAndDeleteRemainingChildren(
returnFiber : Fiber,
currentFirstChild : ?Fiber
) : Map<string, Fiber> {
// Add the remaining children to a temporary map so that we can find them by
// keys quickly. At the same time, we'll flag them all for deletion. However,
// we will then undo the deletion as we restore children. Implicit (null) keys
// don't get added to this set.
const existingChildren : Map<string, Fiber> = new Map();
// TODO: This also needs to store the "previous index of this node". That lets
// us determine whether something needs to be a placement. It might be best to
// just store this on the fiber itself since that lets us use the "implicit"
// index resolution mechanism without adding null values to the linked list.
let existingChild = currentFirstChild;
while (existingChild) {
if (existingChild.key !== null) {
existingChildren.set(existingChild.key, existingChild);
}
// Add everything to the delete queue
// Actually... It is not possible to delete things from the queue since
// we don't have access to the previous link. Does that mean we need a
// second pass to add them? We should be able to keep track of the
// previous deletion as we're iterating through the list the next time.
// That way we know which item to patch when we delete a deletion.
existingChild = existingChild.sibling;
}
return existingChildren;
}
// This wrapper function exists because I expect to clone the code in each path
// to be able to optimize each path individually by branching early. This needs
// a compiler or we can do it manually. Helpers that don't need this branching
// live outside of this function.
function ChildReconciler(shouldClone, shouldTrackSideEffects) {
function useFiber(fiber : Fiber, priority : PriorityLevel) {
// We currently set sibling to null here because it is easy to forget to do
// before returning it.
if (shouldClone) {
const clone = cloneFiber(fiber, priority);
clone.sibling = null;
return clone;
} else {
if (fiber.pendingWorkPriority === NoWork ||
fiber.pendingWorkPriority > priority) {
fiber.pendingWorkPriority = priority;
}
fiber.sibling = null;
return fiber;
}
}
function updateTextNode(
returnFiber : Fiber,
existingChild : ?Fiber,
previousSibling : Fiber,
newChildren : any,
current : ?Fiber,
textContent : string,
priority : PriorityLevel
) : Fiber {
if (typeof newChildren === 'string') {
const textNode = createFiberFromText(newChildren, priority);
previousSibling.sibling = textNode;
textNode.return = returnFiber;
return textNode;
}
if (typeof newChildren !== 'object' || newChildren === null) {
return previousSibling;
}
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement<any>);
if (existingChild &&
element.type === existingChild.type &&
element.key === existingChild.key) {
// TODO: This is not sufficient since previous siblings could be new.
// Will fix reconciliation properly later.
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
if (!shouldClone) {
// TODO: This might be lowering the priority of nested unfinished work.
clone.pendingWorkPriority = priority;
}
clone.pendingProps = element.props;
clone.sibling = null;
clone.return = returnFiber;
previousSibling.sibling = clone;
return clone;
}
const child = createFiberFromElement(element, priority);
previousSibling.sibling = child;
child.return = returnFiber;
return child;
}
case REACT_COROUTINE_TYPE: {
const coroutine = (newChildren : ReactCoroutine);
const child = createFiberFromCoroutine(coroutine, priority);
previousSibling.sibling = child;
child.return = returnFiber;
return child;
}
case REACT_YIELD_TYPE: {
const yieldNode = (newChildren : ReactYield);
const reifiedYield = createReifiedYield(yieldNode);
const child = createFiberFromYield(yieldNode, priority);
child.output = reifiedYield;
previousSibling.sibling = child;
child.return = returnFiber;
return child;
}
}
if (isArray(newChildren)) {
// Nested arrays have a special fiber type.
const fragment = createFiberFromFragment(newChildren, priority);
previousSibling.sibling = fragment;
fragment.return = returnFiber;
return fragment;
) {
if (current == null || current.tag !== HostText) {
// Insert
const created = createFiberFromText(textContent, priority);
created.return = returnFiber;
return created;
} else {
// TODO: Throw for unknown children.
return previousSibling;
// Update
const existing = useFiber(current, priority);
existing.pendingProps = textContent;
existing.return = returnFiber;
return existing;
}
}
function createFirstChild(returnFiber, existingChild, newChildren : any, priority) {
if (typeof newChildren === 'string') {
const textNode = createFiberFromText(newChildren, priority);
textNode.return = returnFiber;
return textNode;
}
if (typeof newChildren !== 'object' || newChildren === null) {
return null;
}
switch (newChildren.$$typeof) {
case REACT_ELEMENT_TYPE: {
const element = (newChildren : ReactElement<any>);
if (existingChild &&
element.type === existingChild.type &&
element.key === existingChild.key) {
// Get the clone of the existing fiber.
const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild;
if (!shouldClone) {
// TODO: This might be lowering the priority of nested unfinished work.
clone.pendingWorkPriority = priority;
}
clone.pendingProps = element.props;
clone.sibling = null;
clone.return = returnFiber;
return clone;
}
const child = createFiberFromElement(element, priority);
child.return = returnFiber;
return child;
}
case REACT_COROUTINE_TYPE: {
const coroutine = (newChildren : ReactCoroutine);
const child = createFiberFromCoroutine(coroutine, priority);
child.return = returnFiber;
return child;
}
case REACT_YIELD_TYPE: {
// A yield results in a fragment fiber whose output is the continuation.
// TODO: When there is only a single child, we can optimize this to avoid
// the fragment.
const yieldNode = (newChildren : ReactYield);
const reifiedYield = createReifiedYield(yieldNode);
const child = createFiberFromYield(yieldNode, priority);
child.output = reifiedYield;
child.return = returnFiber;
return child;
}
}
if (isArray(newChildren)) {
// Nested arrays have a special fiber type.
const fragment = createFiberFromFragment(newChildren, priority);
fragment.return = returnFiber;
return fragment;
function updateElement(
returnFiber : Fiber,
current : ?Fiber,
element : ReactElement<any>,
priority : PriorityLevel
) {
if (current == null || current.type !== element.type) {
// Insert
const created = createFiberFromElement(element, priority);
created.return = returnFiber;
return created;
} else {
// TODO: Throw for unknown children.
return null;
// Move based on index, TODO: This needs to restore a deletion marking.
const existing = useFiber(current, priority);
existing.pendingProps = element.props;
existing.return = returnFiber;
return existing;
}
}
// TODO: This API won't work because we'll need to transfer the side-effects of
// unmounting children to the returnFiber.
function updateCoroutine(
returnFiber : Fiber,
current : ?Fiber,
coroutine : ReactCoroutine,
priority : PriorityLevel
) {
// TODO: Should this also compare handler to determine whether to reuse?
if (current == null || current.tag !== CoroutineComponent) {
// Insert
const created = createFiberFromCoroutine(coroutine, priority);
created.return = returnFiber;
return created;
} else {
// Move based on index, TODO: This needs to restore a deletion marking.
const existing = useFiber(current, priority);
existing.pendingProps = coroutine;
existing.return = returnFiber;
return existing;
}
}
function updateYield(
returnFiber : Fiber,
current : ?Fiber,
yieldNode : ReactYield,
priority : PriorityLevel
) {
// TODO: Should this also compare continuation to determine whether to reuse?
if (current == null || current.tag !== YieldComponent) {
// Insert
const reifiedYield = createReifiedYield(yieldNode);
const created = createFiberFromYield(yieldNode, priority);
created.output = reifiedYield;
created.return = returnFiber;
return created;
} else {
// Move based on index, TODO: This needs to restore a deletion marking.
const existing = useFiber(current, priority);
existing.output = createUpdatedReifiedYield(
current.output,
yieldNode
);
existing.return = returnFiber;
return existing;
}
}
function updateFragment(
returnFiber : Fiber,
current : ?Fiber,
fragment : Iterable<*>,
priority : PriorityLevel
) {
if (current == null || current.tag !== Fragment) {
// Insert
const created = createFiberFromFragment(fragment, priority);
created.return = returnFiber;
return created;
} else {
// Update
const existing = useFiber(current, priority);
existing.pendingProps = fragment;
existing.return = returnFiber;
return existing;
}
}
function updateSlot(
returnFiber : Fiber,
oldFiber : Fiber,
newChild : any,
priority : PriorityLevel
) : ?Fiber {
// Update the fiber if the keys match, otherwise return null.
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys. If the previous node is implicitly keyed
// we can continue to replace it without aborting even if it is not a text
// node.
if (oldFiber.key !== null) {
return null;
}
return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === oldFiber.key) {
return updateElement(
returnFiber,
oldFiber,
newChild,
priority
);
} else {
return null;
}
}
case REACT_COROUTINE_TYPE: {
if (newChild.key === oldFiber.key) {
return updateCoroutine(
returnFiber,
oldFiber,
newChild,
priority
);
} else {
return null;
}
}
case REACT_YIELD_TYPE: {
if (newChild.key === oldFiber.key) {
return updateYield(
returnFiber,
oldFiber,
newChild,
priority
);
} else {
return null;
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
// Fragments doesn't have keys so if the previous key is implicit we can
// update it.
if (oldFiber.key !== null) {
return null;
}
return updateFragment(returnFiber, oldFiber, newChild, priority);
}
}
return null;
}
function updateFromMap(
existingChildren : Map<string, Fiber>,
returnFiber : Fiber,
oldFiber : ?Fiber,
newChild : any,
priority : PriorityLevel
) : ?Fiber {
// TODO: If this child matches, we need to undo the deletion. However,
// we don't do that for the updateSlot case because nothing was deleted yet.
if (typeof newChild === 'string' || typeof newChild === 'number') {
// Text nodes doesn't have keys, so we neither have to check the old nor
// new node for the key. If both are text nodes, they match.
return updateTextNode(returnFiber, oldFiber, '' + newChild, priority);
}
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE: {
if (newChild.key === null) {
// For implicit keys, we'll use the existing fiber in this slot
// but only if it also is an implicit key.
return updateElement(
returnFiber,
oldFiber && oldFiber.key === null ? oldFiber : null,
newChild,
priority
);
} else {
// For explicit keys we look for an existing fiber in the map.
// TODO: We could test oldFiber.key first incase it happens to be
// the same key but it might not be worth it given the likelihood.
const matchedFiber = existingChildren.get(newChild.key);
return updateElement(
returnFiber,
matchedFiber ? matchedFiber : null,
newChild,
priority
);
}
}
case REACT_COROUTINE_TYPE: {
if (newChild.key === null) {
// For implicit keys, we'll use the existing fiber in this slot
// but only if it also is an implicit key.
return updateCoroutine(
returnFiber,
oldFiber && oldFiber.key === null ? oldFiber : null,
newChild,
priority
);
} else {
// For explicit keys we look for an existing fiber in the map.
// TODO: We could test oldFiber.key first incase it happens to be
// the same key but it might not be worth it given the likelihood.
const matchedFiber = existingChildren.get(newChild.key);
return updateCoroutine(
returnFiber,
matchedFiber ? matchedFiber : null,
newChild,
priority
);
}
}
case REACT_YIELD_TYPE: {
if (newChild.key === null) {
// For implicit keys, we'll use the existing fiber in this slot
// but only if it also is an implicit key.
return updateYield(
returnFiber,
oldFiber && oldFiber.key === null ? oldFiber : null,
newChild,
priority
);
} else {
// For explicit keys we look for an existing fiber in the map.
// TODO: We could test oldFiber.key first incase it happens to be
// the same key but it might not be worth it given the likelihood.
const matchedFiber = existingChildren.get(newChild.key);
return updateYield(
returnFiber,
matchedFiber ? matchedFiber : null,
newChild,
priority
);
}
}
}
if (isArray(newChild) || getIteratorFn(newChild)) {
// Fragments doesn't have keys so if the previous is a fragment, we
// update it.
return updateFragment(returnFiber, oldFiber, newChild, priority);
}
}
return null;
}
function reconcileChildrenArray(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
newChildren : Array<*>,
priority : PriorityLevel) {
// This algorithm can't optimize by searching from boths ends since we
// don't have backpointers on fibers. I'm trying to see how far we can get
// with that model. If it ends up not being worth the tradeoffs, we can
// add it later.
// Even with a two ended optimization, we'd want to optimize for the case
// where there are few changes and brute force the comparison instead of
// going for the Map. It'd like to explore hitting that path first in
// forward-only mode and only go for the Map once we notice that we need
// lots of look ahead. This doesn't handle reversal as well as two ended
// search but that's unusual. Besides, for the two ended optimization to
// work on Iterables, we'd need to copy the whole set.
// In this first iteration, we'll just live with hitting the bad case
// (adding everything to a Map) in for every insert/move.
let resultingFirstChild : ?Fiber = null;
let previousNewFiber : ?Fiber = null;
let oldFiber = currentFirstChild;
let newIdx = 0;
for (; oldFiber && newIdx < newChildren.length; newIdx++) {
const nextOldFiber = oldFiber.sibling; // In-case we mutate this fiber.
const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx], priority);
if (!newFiber) {
break;
}
if (!previousNewFiber) {
// TODO: Move out of the loop. This only happens for the first run.
resultingFirstChild = newFiber;
} else {
// TODO: Defer siblings if we're not at the right index for this slot.
// I.e. if we had null values before, then we want to defer this
// for each null value. However, we also don't want to call updateSlot
// with the previous one.
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
oldFiber = nextOldFiber;
}
if (newIdx === newChildren.length) {
// We've reached the end of the new children. We can delete the rest.
deleteRemainingChildren(returnFiber, oldFiber);
return resultingFirstChild;
}
// Mark all children as deleted and add them to a key map for quick lookups.
const existingChildren = mapAndDeleteRemainingChildren(returnFiber, oldFiber);
// Keep scanning and use the map to restore deleted items as moves.
for (; newIdx < newChildren.length; newIdx++) {
// TODO: Since the mutation of existing fibers can happen at any order
// we might break the link before we're done with it. :(
const nextOldFiber = oldFiber ? oldFiber.sibling : null;
const newFiber = updateFromMap(
existingChildren,
returnFiber,
oldFiber,
newChildren[newIdx],
priority
);
if (newFiber) {
if (!previousNewFiber) {
resultingFirstChild = newFiber;
} else {
previousNewFiber.sibling = newFiber;
}
previousNewFiber = newFiber;
}
// We will keep traversing the oldFiber in order, in case the new child
// has a null key that we'll need to match in the same slot.
if (oldFiber) {
oldFiber = nextOldFiber;
}
}
// TODO: Add deletion side-effects to the returnFiber's side-effects.
return resultingFirstChild;
}
function reconcileChildrenIterator(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
newChildren : Iterator<*>,
priority : PriorityLevel) {
// TODO: Copy everything from reconcileChildrenArray but use the iterator
// instead.
return null;
}
function reconcileSingleTextNode(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
textContent : string,
priority : PriorityLevel
) {
// There's no need to check for keys on text nodes since we don't have a
// way to define them.
if (currentFirstChild && currentFirstChild.tag === HostText) {
// We already have an existing node so let's just update it and delete
// the rest.
deleteRemainingChildren(returnFiber, currentFirstChild.sibling);
const existing = useFiber(currentFirstChild, priority);
existing.pendingProps = textContent;
existing.return = returnFiber;
return existing;
}
// The existing first child is not a text node so we need to create one
// and delete the existing ones.
deleteRemainingChildren(returnFiber, currentFirstChild);
const created = createFiberFromText(textContent, priority);
created.return = returnFiber;
return created;
}
function reconcileSingleElement(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
element : ReactElement<any>,
priority : PriorityLevel
) {
const key = element.key;
let child = currentFirstChild;
while (child) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (child.type === element.type) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, priority);
existing.pendingProps = element.props;
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
const created = createFiberFromElement(element, priority);
created.return = returnFiber;
return created;
}
function reconcileSingleCoroutine(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
coroutine : ReactCoroutine,
priority : PriorityLevel
) {
const key = coroutine.key;
let child = currentFirstChild;
while (child) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (child.tag === CoroutineComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, priority);
existing.pendingProps = coroutine;
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
const created = createFiberFromCoroutine(coroutine, priority);
created.return = returnFiber;
return created;
}
function reconcileSingleYield(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
yieldNode : ReactYield,
priority : PriorityLevel
) {
const key = yieldNode.key;
let child = currentFirstChild;
while (child) {
// TODO: If key === null and child.key === null, then this only applies to
// the first item in the list.
if (child.key === key) {
if (child.tag === YieldComponent) {
deleteRemainingChildren(returnFiber, child.sibling);
const existing = useFiber(child, priority);
existing.output = createUpdatedReifiedYield(
child.output,
yieldNode
);
existing.return = returnFiber;
return existing;
} else {
deleteRemainingChildren(returnFiber, child);
break;
}
} else {
deleteChild(returnFiber, child);
}
child = child.sibling;
}
const reifiedYield = createReifiedYield(yieldNode);
const created = createFiberFromYield(yieldNode, priority);
created.output = reifiedYield;
created.return = returnFiber;
return created;
}
// TODO: This API will tag the children with the side-effect of the
// reconciliation itself. Deletes have to get added to the side-effect list
// of the return fiber right now. Other side-effects will be added as we
// pass through those children.
function reconcileChildFibers(
returnFiber : Fiber,
currentFirstChild : ?Fiber,
newChild : ReactNodeList,
newChild : any,
priority : PriorityLevel
) : ?Fiber {
// This function is not recursive.
// If the top level item is an array, we treat it as a set of children,
// not as a fragment. Nested arrays on the other hand will be treated as
// fragment nodes. Recursion happens at the normal flow.
if (isArray(newChild)) {
/* $FlowIssue(>=0.31.0) #12747709
*
* `Array.isArray` is matched syntactically for now until predicate
* support is complete.
*/
var newChildren : Array<*> = newChild;
var first : ?Fiber = null;
var prev : ?Fiber = null;
var existing : ?Fiber = currentFirstChild;
for (var i = 0; i < newChildren.length; i++) {
var nextExisting = existing && existing.sibling;
if (prev == null) {
prev = createFirstChild(returnFiber, existing, newChildren[i], priority);
first = prev;
} else {
prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority);
}
if (prev && existing) {
// TODO: This is not correct because there could've been more
// than one sibling consumed but I don't want to return a tuple.
existing = nextExisting;
}
}
return first;
if (typeof newChild === 'string' || typeof newChild === 'number') {
return reconcileSingleTextNode(
returnFiber,
currentFirstChild,
'' + newChild,
priority
);
}
return createFirstChild(returnFiber, currentFirstChild, newChild, priority);
if (typeof newChild === 'object' && newChild !== null) {
switch (newChild.$$typeof) {
case REACT_ELEMENT_TYPE:
return reconcileSingleElement(
returnFiber,
currentFirstChild,
newChild,
priority
);
case REACT_COROUTINE_TYPE:
return reconcileSingleCoroutine(
returnFiber,
currentFirstChild,
newChild,
priority
);
case REACT_YIELD_TYPE:
return reconcileSingleYield(
returnFiber,
currentFirstChild,
newChild,
priority
);
}
if (isArray(newChild)) {
return reconcileChildrenArray(
returnFiber,
currentFirstChild,
newChild,
priority
);
}
const iteratorFn = getIteratorFn(newChild);
if (iteratorFn) {
return reconcileChildrenIterator(
returnFiber,
currentFirstChild,
newChild,
priority
);
}
}
// Remaining cases are all treated as empty.
return deleteRemainingChildren(returnFiber, currentFirstChild);
}
return reconcileChildFibers;
}
exports.reconcileChildFibers = ChildReconciler(true);
exports.reconcileChildFibers = ChildReconciler(true, true);
exports.reconcileChildFibersInPlace = ChildReconciler(false);
exports.reconcileChildFibersInPlace = ChildReconciler(false, true);
function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) {
workInProgress.return = returnFiber;
while (current.sibling) {
current = current.sibling;
workInProgress = workInProgress.sibling = cloneFiber(
current,
current.pendingWorkPriority
);
workInProgress.return = returnFiber;
}
workInProgress.sibling = null;
}
exports.mountChildFibersInPlace = ChildReconciler(false, false);
exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
if (!workInProgress.child) {
@ -252,7 +726,7 @@ exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
// We use workInProgress.child since that lets Flow know that it can't be
// null since we validated that already. However, as the line above suggests
// they're actually the same thing.
const currentChild = workInProgress.child;
let currentChild = workInProgress.child;
// TODO: This used to reset the pending priority. Not sure if that is needed.
// workInProgress.pendingWorkPriority = current.pendingWorkPriority;
// TODO: The below priority used to be set to NoWork which would've
@ -260,9 +734,19 @@ exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) {
// observable when the first sibling has lower priority work remaining
// than the next sibling. At that point we should add tests that catches
// this.
const newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority);
let newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority);
workInProgress.child = newChild;
cloneSiblings(currentChild, newChild, workInProgress);
newChild.return = workInProgress;
while (currentChild.sibling) {
currentChild = currentChild.sibling;
newChild = newChild.sibling = cloneFiber(
currentChild,
currentChild.pendingWorkPriority
);
newChild.return = workInProgress;
}
newChild.sibling = null;
}
// If there is no alternate, then we don't need to clone the children.

View File

@ -21,6 +21,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel';
import type { UpdateQueue } from 'ReactFiberUpdateQueue';
var {
mountChildFibersInPlace,
reconcileChildFibers,
reconcileChildFibersInPlace,
cloneChildFibers,
@ -74,7 +75,18 @@ module.exports = function<T, P, I, TI, C>(config : HostConfig<T, P, I, TI, C>, g
// At this point any memoization is no longer valid since we'll have changed
// the children.
workInProgress.memoizedProps = null;
if (current && current.child === workInProgress.child) {
if (!current) {
// If this is a fresh new component that hasn't been rendered yet, we
// won't update its child set by applying minimal side-effects. Instead,
// we will add them all to the child before it gets rendered. That means
// we can optimize this reconciliation pass by not tracking side-effects.
workInProgress.child = mountChildFibersInPlace(
workInProgress,
workInProgress.child,
nextChildren,
priorityLevel
);
} else if (current.child === workInProgress.child) {
// If the current child is the same as the work in progress, it means that
// we haven't yet started any work on these children. Therefore, we use
// the clone algorithm to create a copy of all the current children.

View File

@ -31,8 +31,15 @@ exports.createReifiedYield = function(yieldNode : ReactYield) : ReifiedYield {
};
exports.createUpdatedReifiedYield = function(previousYield : ReifiedYield, yieldNode : ReactYield) : ReifiedYield {
var fiber = previousYield.continuation;
if (fiber.type !== yieldNode.continuation) {
fiber = createFiberFromElementType(
yieldNode.continuation,
yieldNode.key
);
}
return {
continuation: previousYield.continuation,
continuation: fiber,
props: yieldNode.props,
};
};