UNPKG

react-native

Version:

A framework for building native apps using React

1,369 lines (1,254 loc) • 44.7 kB
/** * Copyright 2013-present, Facebook, Inc. * All rights reserved. * * This source code is licensed under the BSD-style license found in the * LICENSE file in the root directory of this source tree. An additional grant * of patent rights can be found in the PATENTS file in the same directory. * * @providesModule ReactChildFiber * @flow */ 'use strict'; import type { ReactElement } from 'ReactElementType'; import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { ReactPortal } from 'ReactPortal'; import type { Fiber } from 'ReactFiber'; import type { ReactInstance } from 'ReactInstanceType'; import type { PriorityLevel } from 'ReactPriorityLevel'; var REACT_ELEMENT_TYPE = require('ReactElementSymbol'); var { REACT_COROUTINE_TYPE, REACT_YIELD_TYPE, } = require('ReactCoroutine'); var { REACT_PORTAL_TYPE, } = require('ReactPortal'); var ReactFiber = require('ReactFiber'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var ReactTypeOfWork = require('ReactTypeOfWork'); var emptyObject = require('fbjs/lib/emptyObject'); var getIteratorFn = require('getIteratorFn'); var invariant = require('fbjs/lib/invariant'); var ReactFeatureFlags = require('ReactFeatureFlags'); var ReactCurrentOwner = require('react/lib/ReactCurrentOwner'); if (__DEV__) { var { getCurrentFiberStackAddendum } = require('ReactDebugCurrentFiber'); var { getComponentName } = require('ReactFiberTreeReflection'); var warning = require('fbjs/lib/warning'); var didWarnAboutMaps = false; } const { cloneFiber, createFiberFromElement, createFiberFromFragment, createFiberFromText, createFiberFromCoroutine, createFiberFromYield, createFiberFromPortal, } = ReactFiber; const isArray = Array.isArray; const { FunctionalComponent, ClassComponent, HostText, HostPortal, CoroutineComponent, YieldComponent, Fragment, } = ReactTypeOfWork; const { NoEffect, Placement, Deletion, } = ReactTypeOfSideEffect; function coerceRef(current: Fiber | null, element: ReactElement) { let mixedRef = element.ref; if (mixedRef !== null && typeof mixedRef !== 'function') { if (element._owner) { const owner : ?(Fiber | ReactInstance) = (element._owner : any); let inst; if (owner) { if (typeof owner.tag === 'number') { const ownerFiber = ((owner : any) : Fiber); invariant(ownerFiber.tag === ClassComponent, 'Stateless function components cannot have refs.'); inst = ownerFiber.stateNode; } else { // Stack inst = (owner : any).getPublicInstance(); } } invariant( inst, 'Missing owner for string ref %s. This error is likely caused by a ' + 'bug in React. Please file an issue.', mixedRef ); const stringRef = String(mixedRef); // Check if previous string ref matches new string ref if (current !== null && current.ref !== null && current.ref._stringRef === stringRef) { return current.ref; } const ref = function(value) { const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs; if (value === null) { delete refs[stringRef]; } else { refs[stringRef] = value; } }; ref._stringRef = stringRef; return ref; } } return mixedRef; } function throwOnInvalidObjectType(returnFiber : Fiber, newChild : Object) { if (returnFiber.type !== 'textarea') { const childrenString = String(newChild); let addendum = ''; if (__DEV__) { addendum = ' If you meant to render a collection of children, use an array ' + 'instead or wrap the object using createFragment(object) from the ' + 'React add-ons.'; const owner = ReactCurrentOwner.owner || returnFiber._debugOwner; if (owner && typeof owner.tag === 'number') { const name = getComponentName((owner : any)); if (name) { addendum += '\n\nCheck the render method of `' + name + '`.'; } } } invariant( false, 'Objects are not valid as a React child (found: %s).%s', childrenString === '[object Object]' ? 'object with keys {' + Object.keys(newChild).join(', ') + '}' : childrenString, addendum ); } } // 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 deleteChild( returnFiber : Fiber, childToDelete : Fiber ) : void { if (!shouldTrackSideEffects) { // Noop. return; } if (!shouldClone) { // When we're reconciling in place we have a work in progress copy. We // actually want the current copy. If there is no current copy, then we // don't need to track deletion side-effects. if (childToDelete.alternate === null) { return; } childToDelete = childToDelete.alternate; } // Deletions are added in reversed order so we add it to the front. const last = returnFiber.progressedLastDeletion; if (last !== null) { last.nextEffect = childToDelete; returnFiber.progressedLastDeletion = childToDelete; } else { returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete; } childToDelete.nextEffect = null; childToDelete.effectTag = Deletion; } function deleteRemainingChildren( returnFiber : Fiber, currentFirstChild : Fiber | null ) : null { if (!shouldTrackSideEffects) { // Noop. return null; } // TODO: For the shouldClone case, this could be micro-optimized a bit by // assuming that after the first child we've already added everything. let childToDelete = currentFirstChild; while (childToDelete !== null) { deleteChild(returnFiber, childToDelete); childToDelete = childToDelete.sibling; } return null; } function mapRemainingChildren( returnFiber : Fiber, currentFirstChild : Fiber ) : Map<string | number, Fiber> { // Add the remaining children to a temporary map so that we can find them by // keys quickly. Implicit (null) keys get added to this set with their index // instead. const existingChildren : Map<string | number, Fiber> = new Map(); let existingChild = currentFirstChild; while (existingChild !== null) { if (existingChild.key !== null) { existingChildren.set(existingChild.key, existingChild); } else { existingChildren.set(existingChild.index, existingChild); } existingChild = existingChild.sibling; } return existingChildren; } function useFiber(fiber : Fiber, priority : PriorityLevel) : Fiber { // We currently set sibling to null and index to 0 here because it is easy // to forget to do before returning it. E.g. for the single child case. if (shouldClone) { const clone = cloneFiber(fiber, priority); clone.index = 0; clone.sibling = null; return clone; } else { // We override the pending priority even if it is higher, because if // we're reconciling at a lower priority that means that this was // down-prioritized. fiber.pendingWorkPriority = priority; fiber.effectTag = NoEffect; fiber.index = 0; fiber.sibling = null; return fiber; } } function placeChild(newFiber : Fiber, lastPlacedIndex : number, newIndex : number) : number { newFiber.index = newIndex; if (!shouldTrackSideEffects) { // Noop. return lastPlacedIndex; } const current = newFiber.alternate; if (current !== null) { const oldIndex = current.index; if (oldIndex < lastPlacedIndex) { // This is a move. newFiber.effectTag = Placement; return lastPlacedIndex; } else { // This item can stay in place. return oldIndex; } } else { // This is an insertion. newFiber.effectTag = Placement; return lastPlacedIndex; } } function placeSingleChild(newFiber : Fiber) : Fiber { // This is simpler for the single child case. We only need to do a // placement for inserting new children. if (shouldTrackSideEffects && newFiber.alternate === null) { newFiber.effectTag = Placement; } return newFiber; } function updateTextNode( returnFiber : Fiber, current : Fiber | null, textContent : string, priority : PriorityLevel ) { if (current === null || current.tag !== HostText) { // Insert const created = createFiberFromText(textContent, priority); created.return = returnFiber; return created; } else { // Update const existing = useFiber(current, priority); existing.pendingProps = textContent; existing.return = returnFiber; return existing; } } function updateElement( returnFiber : Fiber, current : Fiber | null, element : ReactElement, priority : PriorityLevel ) : Fiber { if (current === null || current.type !== element.type) { // Insert const created = createFiberFromElement(element, priority); created.ref = coerceRef(current, element); created.return = returnFiber; return created; } else { // Move based on index const existing = useFiber(current, priority); existing.ref = coerceRef(current, element); existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; existing._debugOwner = element._owner; } return existing; } } function updateCoroutine( returnFiber : Fiber, current : Fiber | null, coroutine : ReactCoroutine, priority : PriorityLevel ) : Fiber { // 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 const existing = useFiber(current, priority); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; } } function updateYield( returnFiber : Fiber, current : Fiber | null, yieldNode : ReactYield, priority : PriorityLevel ) : Fiber { if (current === null || current.tag !== YieldComponent) { // Insert const created = createFiberFromYield(yieldNode, priority); created.type = yieldNode.value; created.return = returnFiber; return created; } else { // Move based on index const existing = useFiber(current, priority); existing.type = yieldNode.value; existing.return = returnFiber; return existing; } } function updatePortal( returnFiber : Fiber, current : Fiber | null, portal : ReactPortal, priority : PriorityLevel ) : Fiber { if ( current === null || current.tag !== HostPortal || current.stateNode.containerInfo !== portal.containerInfo || current.stateNode.implementation !== portal.implementation ) { // Insert const created = createFiberFromPortal(portal, priority); created.return = returnFiber; return created; } else { // Update const existing = useFiber(current, priority); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; } } function updateFragment( returnFiber : Fiber, current : Fiber | null, fragment : Iterable<*>, priority : PriorityLevel ) : Fiber { 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 createChild( returnFiber : Fiber, newChild : any, priority : PriorityLevel ) : Fiber | 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. const created = createFiberFromText('' + newChild, priority); created.return = returnFiber; return created; } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { const created = createFiberFromElement(newChild, priority); created.ref = coerceRef(null, newChild); created.return = returnFiber; return created; } case REACT_COROUTINE_TYPE: { const created = createFiberFromCoroutine(newChild, priority); created.return = returnFiber; return created; } case REACT_YIELD_TYPE: { const created = createFiberFromYield(newChild, priority); created.type = newChild.value; created.return = returnFiber; return created; } case REACT_PORTAL_TYPE: { const created = createFiberFromPortal(newChild, priority); created.return = returnFiber; return created; } } if (isArray(newChild) || getIteratorFn(newChild)) { const created = createFiberFromFragment(newChild, priority); created.return = returnFiber; return created; } throwOnInvalidObjectType(returnFiber, newChild); } return null; } function updateSlot( returnFiber : Fiber, oldFiber : Fiber | null, newChild : any, priority : PriorityLevel ) : Fiber | null { // Update the fiber if the keys match, otherwise return null. const key = oldFiber !== null ? oldFiber.key : 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 (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 === key) { return updateElement(returnFiber, oldFiber, newChild, priority); } else { return null; } } case REACT_COROUTINE_TYPE: { if (newChild.key === key) { return updateCoroutine(returnFiber, oldFiber, newChild, priority); } else { return null; } } case REACT_YIELD_TYPE: { // Yields 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 // yield. if (key === null) { return updateYield(returnFiber, oldFiber, newChild, priority); } else { return null; } } case REACT_PORTAL_TYPE: { if (newChild.key === key) { return updatePortal(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 (key !== null) { return null; } return updateFragment(returnFiber, oldFiber, newChild, priority); } throwOnInvalidObjectType(returnFiber, newChild); } return null; } function updateFromMap( existingChildren : Map<string | number, Fiber>, returnFiber : Fiber, newIdx : number, newChild : any, priority : PriorityLevel ) : Fiber | null { 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. const matchedFiber = existingChildren.get(newIdx) || null; return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority); } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key ) || null; return updateElement(returnFiber, matchedFiber, newChild, priority); } case REACT_COROUTINE_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key ) || null; return updateCoroutine(returnFiber, matchedFiber, newChild, priority); } case REACT_YIELD_TYPE: { // Yields doesn't have keys, so we neither have to check the old nor // new node for the key. If both are yields, they match. const matchedFiber = existingChildren.get(newIdx) || null; return updateYield(returnFiber, matchedFiber, newChild, priority); } case REACT_PORTAL_TYPE: { const matchedFiber = existingChildren.get( newChild.key === null ? newIdx : newChild.key ) || null; return updatePortal(returnFiber, matchedFiber, newChild, priority); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; return updateFragment(returnFiber, matchedFiber, newChild, priority); } throwOnInvalidObjectType(returnFiber, newChild); } return null; } function warnOnDuplicateKey( child : mixed, knownKeys : Set<string> | null ) : Set<string> | null { if (__DEV__) { if (typeof child !== 'object' || child === null) { return knownKeys; } switch (child.$$typeof) { case REACT_ELEMENT_TYPE: case REACT_COROUTINE_TYPE: case REACT_PORTAL_TYPE: const key = child.key; if (typeof key !== 'string') { break; } if (knownKeys === null) { knownKeys = new Set(); knownKeys.add(key); break; } if (!knownKeys.has(key)) { knownKeys.add(key); break; } warning( false, 'Encountered two children with the same key, ' + '`%s`. Child keys must be unique; when two children share a key, ' + 'only the first child will be used.%s', key, getCurrentFiberStackAddendum() ); break; default: break; } } return knownKeys; } function reconcileChildrenArray( returnFiber : Fiber, currentFirstChild : Fiber | null, newChildren : Array<*>, priority : PriorityLevel) : Fiber | null { // 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. // If you change this code, also update reconcileChildrenIterator() which // uses the same algorithm. if (__DEV__) { // First, validate keys. let knownKeys = null; for (let i = 0; i < newChildren.length; i++) { const child = newChildren[i]; knownKeys = warnOnDuplicateKey(child, knownKeys); } } let resultingFirstChild : Fiber | null = null; let previousNewFiber : Fiber | null = null; let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } const newFiber = updateSlot( returnFiber, oldFiber, newChildren[newIdx], priority ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need // a better way to communicate whether this was a miss or null, // boolean, undefined, etc. if (oldFiber === null) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. deleteChild(returnFiber, oldFiber); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // 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; } if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; newIdx < newChildren.length; newIdx++) { const newFiber = createChild( returnFiber, newChildren[newIdx], priority ); if (!newFiber) { continue; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; } // Add all children to a key map for quick lookups. const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. for (; newIdx < newChildren.length; newIdx++) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, newChildren[newIdx], priority ); if (newFiber) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // The new fiber is a work in progress, but if there exists a // current, that means that we reused the fiber. We need to delete // it from the child list so that we don't add it to the deletion // list. existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { // Any existing children that weren't consumed above were deleted. We need // to add them to the deletion list. existingChildren.forEach(child => deleteChild(returnFiber, child)); } return resultingFirstChild; } function reconcileChildrenIterator( returnFiber : Fiber, currentFirstChild : Fiber | null, newChildrenIterable : Iterable<*>, priority : PriorityLevel) : Fiber | null { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. const iteratorFn = getIteratorFn(newChildrenIterable); invariant( typeof iteratorFn === 'function', 'An object is not an iterable. This error is likely caused by a bug in ' + 'React. Please file an issue.' ); if (__DEV__) { // Warn about using Maps as children if (typeof newChildrenIterable.entries === 'function') { const possibleMap = (newChildrenIterable : any); if (possibleMap.entries === iteratorFn) { let mapsAsChildrenAddendum = ''; const owner = ReactCurrentOwner.owner || returnFiber._debugOwner; if (owner && typeof owner.tag === 'number') { const mapsAsChildrenOwnerName = getComponentName((owner : any)); if (mapsAsChildrenOwnerName) { mapsAsChildrenAddendum = '\n\nCheck the render method of `' + mapsAsChildrenOwnerName + '`.'; } } warning( didWarnAboutMaps, 'Using Maps as children is unsupported and will likely yield ' + 'unexpected results. Convert it to a sequence/iterable of keyed ' + 'ReactElements instead.%s', mapsAsChildrenAddendum ); didWarnAboutMaps = true; } } // First, validate keys. // We'll get a different iterator later for the main pass. const newChildren = iteratorFn.call(newChildrenIterable); if (newChildren) { let knownKeys = null; let step = newChildren.next(); for (; !step.done; step = newChildren.next()) { const child = step.value; knownKeys = warnOnDuplicateKey(child, knownKeys); } } } const newChildren = iteratorFn.call(newChildrenIterable); invariant( newChildren != null, 'An iterable object provided no iterator.', ); let resultingFirstChild : Fiber | null = null; let previousNewFiber : Fiber | null = null; let oldFiber = currentFirstChild; let lastPlacedIndex = 0; let newIdx = 0; let nextOldFiber = null; let step = newChildren.next(); for (; oldFiber !== null && !step.done; newIdx++, step = newChildren.next()) { if (oldFiber.index > newIdx) { nextOldFiber = oldFiber; oldFiber = null; } else { nextOldFiber = oldFiber.sibling; } const newFiber = updateSlot( returnFiber, oldFiber, step.value, priority ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need // a better way to communicate whether this was a miss or null, // boolean, undefined, etc. if (!oldFiber) { oldFiber = nextOldFiber; } break; } if (shouldTrackSideEffects) { if (oldFiber && newFiber.alternate === null) { // We matched the slot, but we didn't reuse the existing fiber, so we // need to delete the existing child. deleteChild(returnFiber, oldFiber); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // 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 (step.done) { // We've reached the end of the new children. We can delete the rest. deleteRemainingChildren(returnFiber, oldFiber); return resultingFirstChild; } if (oldFiber === null) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; !step.done; newIdx++, step = newChildren.next()) { const newFiber = createChild( returnFiber, step.value, priority ); if (newFiber === null) { continue; } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { // TODO: Move out of the loop. This only happens for the first run. resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } return resultingFirstChild; } // Add all children to a key map for quick lookups. const existingChildren = mapRemainingChildren(returnFiber, oldFiber); // Keep scanning and use the map to restore deleted items as moves. for (; !step.done; newIdx++, step = newChildren.next()) { const newFiber = updateFromMap( existingChildren, returnFiber, newIdx, step.value, priority ); if (newFiber !== null) { if (shouldTrackSideEffects) { if (newFiber.alternate !== null) { // The new fiber is a work in progress, but if there exists a // current, that means that we reused the fiber. We need to delete // it from the child list so that we don't add it to the deletion // list. existingChildren.delete( newFiber.key === null ? newIdx : newFiber.key ); } } lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); if (previousNewFiber === null) { resultingFirstChild = newFiber; } else { previousNewFiber.sibling = newFiber; } previousNewFiber = newFiber; } } if (shouldTrackSideEffects) { // Any existing children that weren't consumed above were deleted. We need // to add them to the deletion list. existingChildren.forEach(child => deleteChild(returnFiber, child)); } return resultingFirstChild; } function reconcileSingleTextNode( returnFiber : Fiber, currentFirstChild : Fiber | null, textContent : string, priority : PriorityLevel ) : Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. if (currentFirstChild !== null && 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 | null, element : ReactElement, priority : PriorityLevel ) : Fiber { const key = element.key; let child = currentFirstChild; while (child !== null) { // 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.ref = coerceRef(child, element); existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; existing._debugOwner = element._owner; } return existing; } else { deleteRemainingChildren(returnFiber, child); break; } } else { deleteChild(returnFiber, child); } child = child.sibling; } const created = createFiberFromElement(element, priority); created.ref = coerceRef(currentFirstChild, element); created.return = returnFiber; return created; } function reconcileSingleCoroutine( returnFiber : Fiber, currentFirstChild : Fiber | null, coroutine : ReactCoroutine, priority : PriorityLevel ) : Fiber { const key = coroutine.key; let child = currentFirstChild; while (child !== null) { // 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 | null, yieldNode : ReactYield, priority : PriorityLevel ) : Fiber { // There's no need to check for keys on yields since they're stateless. let child = currentFirstChild; if (child !== null) { if (child.tag === YieldComponent) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, priority); existing.type = yieldNode.value; existing.return = returnFiber; return existing; } else { deleteRemainingChildren(returnFiber, child); } } const created = createFiberFromYield(yieldNode, priority); created.type = yieldNode.value; created.return = returnFiber; return created; } function reconcileSinglePortal( returnFiber : Fiber, currentFirstChild : Fiber | null, portal : ReactPortal, priority : PriorityLevel ) : Fiber { const key = portal.key; let child = currentFirstChild; while (child !== null) { // 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 === HostPortal && child.stateNode.containerInfo === portal.containerInfo && child.stateNode.implementation === portal.implementation ) { deleteRemainingChildren(returnFiber, child.sibling); const existing = useFiber(child, priority); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; } else { deleteRemainingChildren(returnFiber, child); break; } } else { deleteChild(returnFiber, child); } child = child.sibling; } const created = createFiberFromPortal(portal, priority); created.return = returnFiber; return created; } // This API will tag the children with the side-effect of the reconciliation // itself. They will be added to the side-effect list as we pass through the // children and the parent. function reconcileChildFibers( returnFiber : Fiber, currentFirstChild : Fiber | null, newChild : any, priority : PriorityLevel ) : Fiber | null { // 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. const disableNewFiberFeatures = ReactFeatureFlags.disableNewFiberFeatures; // Handle object types const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { // Support only the subset of return types that Stack supports. Treat // everything else as empty, but log a warning. if (disableNewFiberFeatures) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild(reconcileSingleElement( returnFiber, currentFirstChild, newChild, priority )); case REACT_PORTAL_TYPE: return placeSingleChild(reconcileSinglePortal( returnFiber, currentFirstChild, newChild, priority )); } } else { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild(reconcileSingleElement( returnFiber, currentFirstChild, newChild, priority )); case REACT_COROUTINE_TYPE: return placeSingleChild(reconcileSingleCoroutine( returnFiber, currentFirstChild, newChild, priority )); case REACT_YIELD_TYPE: return placeSingleChild(reconcileSingleYield( returnFiber, currentFirstChild, newChild, priority )); case REACT_PORTAL_TYPE: return placeSingleChild(reconcileSinglePortal( returnFiber, currentFirstChild, newChild, priority )); } } } if (disableNewFiberFeatures) { // The new child is not an element. If it's not null or false, // and the return fiber is a composite component, throw an error. switch (returnFiber.tag) { case ClassComponent: { if (__DEV__) { const instance = returnFiber.stateNode; if (instance.render._isMockFunction && typeof newChild === 'undefined') { // We allow auto-mocks to proceed as if they're // returning null. break; } } const Component = returnFiber.type; invariant( newChild === null || newChild === false, '%s.render(): A valid React element (or null) must be returned. ' + 'You may have returned undefined, an array or some other ' + 'invalid object.', Component.displayName || Component.name || 'Component', ); break; } case FunctionalComponent: { // Composites accept elements, portals, null, or false const Component = returnFiber.type; invariant( newChild === null || newChild === false, '%s(...): A valid React element (or null) must be returned. ' + 'You may have returned undefined, an array or some other ' + 'invalid object.', Component.displayName || Component.name || 'Component', ); break; } } } if (typeof newChild === 'string' || typeof newChild === 'number') { return placeSingleChild(reconcileSingleTextNode( returnFiber, currentFirstChild, '' + newChild, priority )); } if (isArray(newChild)) { return reconcileChildrenArray( returnFiber, currentFirstChild, newChild, priority ); } if (getIteratorFn(newChild)) { return reconcileChildrenIterator( returnFiber, currentFirstChild, newChild, priority ); } if (isObject) { throwOnInvalidObjectType(returnFiber, newChild); } if (!disableNewFiberFeatures && typeof newChild === 'undefined') { // If the new child is undefined, and the return fiber is a composite // component, throw an error. If Fiber return types are disabled, // we already threw above. switch (returnFiber.tag) { case ClassComponent: { if (__DEV__) { const instance = returnFiber.stateNode; if (instance.render._isMockFunction) { // We allow auto-mocks to proceed as if they're returning null. break; } } } // Intentionally fall through to the next case, which handles both // functions and classes // eslint-disable-next-lined no-fallthrough case FunctionalComponent: { const Component = returnFiber.type; invariant( false, '%s(...): Nothing was returned from render. This usually means a ' + 'return statement is missing. Or, to render nothing, ' + 'return null.', Component.displayName || Component.name || 'Component' ); } } } // Remaining cases are all treated as empty. return deleteRemainingChildren(returnFiber, currentFirstChild); } return reconcileChildFibers; } exports.reconcileChildFibers = ChildReconciler(true, true); exports.reconcileChildFibersInPlace = ChildReconciler(false, true); exports.mountChildFibersInPlace = ChildReconciler(false, false); exports.cloneChildFibers = function(current : Fiber | null, workInProgress : Fiber) : void { if (!workInProgress.child) { return; } if (current !== null && workInProgress.child === current.child) { // 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. 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 // dropped work. This is currently unobservable but will become // observable when the first sibling has lower priority work remaining // than the next sibling. At that point we should add tests that catches // this. let newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority); workInProgress.child = newChild; newChild.return = workInProgress; while (currentChild.sibling !== null) { 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. // If the children of the alternate fiber is a different set, then we don't // need to clone. We need to reset the return fiber though since we'll // traverse down into them. let child = workInProgress.child; while (child !== null) { child.return = workInProgress; child = child.sibling; } };