UNPKG

react-native-macos

Version:

A framework for building native macOS apps using React

1,471 lines (1,358 loc) • 46.6 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'); if (__DEV__) { var {getCurrentFiberStackAddendum} = require('ReactDebugCurrentFiber'); var warning = require('fbjs/lib/warning'); var didWarnAboutMaps = false; /** * Warn if there's no key explicitly set on dynamic arrays of children or * object keys are not valid. This allows us to keep track of children between * updates. */ var ownerHasKeyUseWarning = {}; var warnForMissingKey = (child: mixed) => { if (child === null || typeof child !== 'object') { return; } if (!child._store || child._store.validated || child.key != null) { return; } invariant( typeof child._store === 'object', 'React Component in warnForMissingKey should have a _store', ); child._store.validated = true; var currentComponentErrorInfo = 'Each child in an array or iterator should have a unique ' + '"key" prop. See https://fb.me/react-warning-keys for ' + 'more information.' + (getCurrentFiberStackAddendum(child) || ''); if (ownerHasKeyUseWarning[currentComponentErrorInfo]) { return; } ownerHasKeyUseWarning[currentComponentErrorInfo] = true; warning( false, 'Each child in an array or iterator should have a unique ' + '"key" prop. See https://fb.me/react-warning-keys for ' + 'more information.%s', getCurrentFiberStackAddendum(child), ); }; } 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 = '' + 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') { let addendum = ''; if (__DEV__) { addendum = ' If you meant to render a collection of children, use an array ' + 'instead.' + (getCurrentFiberStackAddendum() || ''); } invariant( false, 'Objects are not valid as a React child (found: %s).%s', Object.prototype.toString.call(newChild) === '[object Object]' ? 'object with keys {' + Object.keys(newChild).join(', ') + '}' : newChild, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, priority, ); created.return = returnFiber; return created; } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { const created = createFiberFromElement( newChild, returnFiber.internalContextTag, priority, ); created.ref = coerceRef(null, newChild); created.return = returnFiber; return created; } case REACT_COROUTINE_TYPE: { const created = createFiberFromCoroutine( newChild, returnFiber.internalContextTag, priority, ); created.return = returnFiber; return created; } case REACT_YIELD_TYPE: { const created = createFiberFromYield( newChild, returnFiber.internalContextTag, priority, ); created.type = newChild.value; created.return = returnFiber; return created; } case REACT_PORTAL_TYPE: { const created = createFiberFromPortal( newChild, returnFiber.internalContextTag, priority, ); created.return = returnFiber; return created; } } if (isArray(newChild) || getIteratorFn(newChild)) { const created = createFiberFromFragment( newChild, returnFiber.internalContextTag, 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; } /** * Warns if there is a duplicate or missing key */ function warnOnInvalidKey( 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: warnForMissingKey(child); 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 = warnOnInvalidKey(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) { 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', getCurrentFiberStackAddendum(), ); 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 = warnOnInvalidKey(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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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, returnFiber.internalContextTag, 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; } else { // 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; } } };