diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index d4b9c7584dbcf..b2cfac0ed83cf 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -15,6 +15,7 @@ import type { HostChildren } from 'ReactFiberReconciler'; var ReactFiberReconciler = require('ReactFiberReconciler'); +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); var warning = require('warning'); @@ -23,13 +24,14 @@ type DOMContainerElement = Element & { _reactRootContainer: ?Object }; type Container = Element; type Props = { }; type Instance = Element; +type TextInstance = Text; -function recursivelyAppendChildren(parent : Element, child : HostChildren) { +function recursivelyAppendChildren(parent : Element, child : HostChildren) { if (!child) { return; } - /* $FlowFixMe: Element should have this property. */ - if (child.nodeType === 1) { + /* $FlowFixMe: Element and Text should have this property. */ + if (child.nodeType === 1 || child.nodeType === 3) { /* $FlowFixMe: Refinement issue. I don't know how to express different. */ parent.appendChild(child); } else { @@ -43,15 +45,17 @@ function recursivelyAppendChildren(parent : Element, child : HostChildren) : void { + updateContainer(container : Container, children : HostChildren) : void { + // TODO: Containers should update similarly to other parents. container.innerHTML = ''; recursivelyAppendChildren(container, children); }, - createInstance(type : string, props : Props, children : HostChildren) : Instance { + createInstance(type : string, props : Props, children : HostChildren) : Instance { const domElement = document.createElement(type); recursivelyAppendChildren(domElement, children); - if (typeof props.children === 'string') { + if (typeof props.children === 'string' || + typeof props.children === 'number') { domElement.textContent = props.children; } return domElement; @@ -60,22 +64,36 @@ var DOMRenderer = ReactFiberReconciler({ prepareUpdate( domElement : Instance, oldProps : Props, - newProps : Props, - children : HostChildren + newProps : Props ) : boolean { return true; }, - commitUpdate(domElement : Instance, oldProps : Props, newProps : Props, children : HostChildren) : void { - domElement.innerHTML = ''; - recursivelyAppendChildren(domElement, children); - if (typeof newProps.children === 'string') { + commitUpdate(domElement : Instance, oldProps : Props, newProps : Props) : void { + if (typeof newProps.children === 'string' || + typeof newProps.children === 'number') { domElement.textContent = newProps.children; } }, - deleteInstance(instance : Instance) : void { - // Noop + createTextInstance(text : string) : TextInstance { + return document.createTextNode(text); + }, + + commitTextUpdate(textInstance : TextInstance, oldText : string, newText : string) : void { + textInstance.nodeValue = newText; + }, + + appendChild(parentInstance : Instance, child : Instance | TextInstance) : void { + parentInstance.appendChild(child); + }, + + insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void { + parentInstance.insertBefore(child, beforeChild); + }, + + removeChild(parentInstance : Instance, child : Instance | TextInstance) : void { + parentInstance.removeChild(child); }, scheduleAnimationCallback: window.requestAnimationFrame, @@ -87,8 +105,9 @@ var DOMRenderer = ReactFiberReconciler({ var warned = false; function warnAboutUnstableUse() { + // Ignore this warning is the feature flag is turned on. E.g. for tests. warning( - warned, + warned || ReactDOMFeatureFlags.useFiber, 'You are using React DOM Fiber which is an experimental renderer. ' + 'It is likely to have bugs, breaking changes and is unsupported.' ); diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index ff316945ece24..0ac23ac0c3fa8 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -32,18 +32,20 @@ var scheduledAnimationCallback = null; var scheduledDeferredCallback = null; const TERMINAL_TAG = 99; +const TEXT_TAG = 98; -type Container = { rootID: number, children: Array }; +type Container = { rootID: number, children: Array }; type Props = { prop: any }; -type Instance = { tag: 99, type: string, id: number, children: Array, prop: any }; +type Instance = { tag: 99, type: string, id: number, children: Array, prop: any }; +type TextInstance = { tag: 98, text: string }; var instanceCounter = 0; -function recursivelyAppendChildren(flatArray : Array, child : HostChildren) { +function recursivelyAppendChildren(flatArray : Array, child : HostChildren) { if (!child) { return; } - if (child.tag === TERMINAL_TAG) { + if (child.tag === TERMINAL_TAG || child.tag === TEXT_TAG) { flatArray.push(child); } else { let node = child; @@ -53,7 +55,7 @@ function recursivelyAppendChildren(flatArray : Array, child : HostChil } } -function flattenChildren(children : HostChildren) { +function flattenChildren(children : HostChildren) { const flatArray = []; recursivelyAppendChildren(flatArray, children); return flatArray; @@ -61,11 +63,11 @@ function flattenChildren(children : HostChildren) { var NoopRenderer = ReactFiberReconciler({ - updateContainer(containerInfo : Container, children : HostChildren) : void { + updateContainer(containerInfo : Container, children : HostChildren) : void { containerInfo.children = flattenChildren(children); }, - createInstance(type : string, props : Props, children : HostChildren) : Instance { + createInstance(type : string, props : Props, children : HostChildren) : Instance { const inst = { tag: TERMINAL_TAG, id: instanceCounter++, @@ -79,16 +81,51 @@ var NoopRenderer = ReactFiberReconciler({ return inst; }, - prepareUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren) : boolean { + prepareUpdate(instance : Instance, oldProps : Props, newProps : Props) : boolean { return true; }, - commitUpdate(instance : Instance, oldProps : Props, newProps : Props, children : HostChildren) : void { - instance.children = flattenChildren(children); + commitUpdate(instance : Instance, oldProps : Props, newProps : Props) : void { instance.prop = newProps.prop; }, - deleteInstance(instance : Instance) : void { + createTextInstance(text : string) : TextInstance { + var inst = { tag: TEXT_TAG, text : text }; + // Hide from unit tests + Object.defineProperty(inst, 'tag', { value: inst.tag, enumerable: false }); + return inst; + }, + + commitTextUpdate(textInstance : TextInstance, oldText : string, newText : string) : void { + textInstance.text = newText; + }, + + appendChild(parentInstance : Instance, child : Instance | TextInstance) : void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + parentInstance.children.push(child); + }, + + insertBefore(parentInstance : Instance, child : Instance | TextInstance, beforeChild : Instance | TextInstance) : void { + const index = parentInstance.children.indexOf(child); + if (index !== -1) { + parentInstance.children.splice(index, 1); + } + const beforeIndex = parentInstance.children.indexOf(beforeChild); + if (beforeIndex === -1) { + throw new Error('This child does not exist.'); + } + parentInstance.children.splice(beforeIndex, 0, child); + }, + + removeChild(parentInstance : Instance, child : Instance | TextInstance) : void { + const index = parentInstance.children.indexOf(child); + if (index === -1) { + throw new Error('This child does not exist.'); + } + parentInstance.children.splice(index, 1); }, scheduleAnimationCallback(callback) { @@ -166,11 +203,15 @@ var ReactNoop = { bufferedLog.push(...args, '\n'); } - function logHostInstances(children: Array, depth) { + function logHostInstances(children: Array, depth) { for (var i = 0; i < children.length; i++) { var child = children[i]; - log(' '.repeat(depth) + '- ' + child.type + '#' + child.id); - logHostInstances(child.children, depth + 1); + if (child.tag === TEXT_TAG) { + log(' '.repeat(depth) + '- ' + child.text); + } else { + log(' '.repeat(depth) + '- ' + child.type + '#' + child.id); + logHostInstances(child.children, depth + 1); + } } } function logContainer(container : Container, depth) { diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 93839caed5fdb..8dfd9e3a7d1ef 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -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,215 +23,803 @@ var { } = require('ReactCoroutine'); var ReactFiber = require('ReactFiber'); +var ReactPriorityLevel = require('ReactPriorityLevel'); var ReactReifiedYield = require('ReactReifiedYield'); +var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); +var ReactTypeOfWork = require('ReactTypeOfWork'); + +var getIteratorFn = require('getIteratorFn'); const { cloneFiber, createFiberFromElement, + createFiberFromFragment, + createFiberFromText, createFiberFromCoroutine, createFiberFromYield, } = ReactFiber; const { createReifiedYield, + createUpdatedReifiedYield, } = ReactReifiedYield; const isArray = Array.isArray; -function ChildReconciler(shouldClone) { +const { + HostText, + CoroutineComponent, + YieldComponent, + Fragment, +} = ReactTypeOfWork; + +const { + NoWork, +} = ReactPriorityLevel; + +const { + Placement, + Deletion, +} = ReactTypeOfSideEffect; - function createSubsequentChild( +// 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, - existingChild : ?Fiber, - previousSibling : Fiber, - newChildren, + childToDelete : Fiber + ) { + 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) { + return; + } + childToDelete = childToDelete.alternate; + } + // Deletions are added in reversed order so we add it to the front. + const last = returnFiber.progressedLastDeletion; + if (last) { + last.nextEffect = childToDelete; + returnFiber.progressedLastDeletion = childToDelete; + } else { + returnFiber.progressedFirstDeletion = + returnFiber.progressedLastDeletion = + childToDelete; + } + childToDelete.nextEffect = null; + childToDelete.effectTag = Deletion; + } + + function deleteRemainingChildren( + returnFiber : Fiber, + currentFirstChild : ?Fiber + ) { + 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) { + deleteChild(returnFiber, childToDelete); + childToDelete = childToDelete.sibling; + } + return null; + } + + function mapRemainingChildren( + returnFiber : Fiber, + currentFirstChild : Fiber + ) : Map { + // 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 = new Map(); + + let existingChild = currentFirstChild; + while (existingChild) { + 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) { + // 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 = NoWork; + fiber.index = 0; + fiber.sibling = null; + return fiber; + } + } + + function placeChild(newFiber : Fiber, lastPlacedIndex : number, newIndex : number) { + newFiber.index = newIndex; + if (!shouldTrackSideEffects) { + // Noop. + return lastPlacedIndex; + } + const current = newFiber.alternate; + if (current) { + 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) { + // This is simpler for the single child case. We only need to do a + // placement for inserting new children. + if (shouldTrackSideEffects && !newFiber.alternate) { + newFiber.effectTag = Placement; + } + return newFiber; + } + + function updateTextNode( + returnFiber : Fiber, + current : ?Fiber, + textContent : string, priority : PriorityLevel - ) : Fiber { - if (typeof newChildren !== 'object' || newChildren === null) { - return previousSibling; - } - - switch (newChildren.$$typeof) { - case REACT_ELEMENT_TYPE: { - const element = (newChildren : ReactElement); - 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; + ) { + 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, + element : ReactElement, + priority : PriorityLevel + ) { + if (current == null || current.type !== element.type) { + // Insert + const created = createFiberFromElement(element, priority); + created.ref = element.ref; + created.return = returnFiber; + return created; + } else { + // Move based on index + const existing = useFiber(current, priority); + existing.ref = element.ref; + existing.pendingProps = element.props; + existing.return = returnFiber; + return existing; + } + } + + 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 + 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 + 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 createChild( + returnFiber : Fiber, + newChild : any, + priority : PriorityLevel + ) : ?Fiber { + 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 = newChild.ref; + created.return = returnFiber; + return created; + } + + case REACT_COROUTINE_TYPE: { + const created = createFiberFromCoroutine(newChild, priority); + created.return = returnFiber; + return created; + } + + case REACT_YIELD_TYPE: { + const reifiedYield = createReifiedYield(newChild); + const created = createFiberFromYield(newChild, priority); + created.output = reifiedYield; + created.return = returnFiber; + return created; } - 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; + if (isArray(newChild) || getIteratorFn(newChild)) { + const created = createFiberFromFragment(newChild, priority); + created.return = returnFiber; + return created; } + } + + return null; + } + + function updateSlot( + returnFiber : Fiber, + oldFiber : ?Fiber, + newChild : any, + priority : PriorityLevel + ) : ?Fiber { + // Update the fiber if the keys match, otherwise return null. + + const key = oldFiber ? oldFiber.key : null; - 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 (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 (isArray(newChildren)) { - let prev : Fiber = previousSibling; - let existing : ?Fiber = existingChild; - for (var i = 0; i < newChildren.length; i++) { - var nextExisting = existing && existing.sibling; - 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; + 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: { + if (newChild.key === key) { + return updateYield(returnFiber, oldFiber, newChild, priority); + } else { + return null; + } } } - return prev; - } else { - // TODO: Throw for unknown children. - return previousSibling; + + 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); + } } + + return null; } - function createFirstChild(returnFiber, existingChild, newChildren, priority) { - if (typeof newChildren !== 'object' || newChildren === null) { - return null; + function updateFromMap( + existingChildren : Map, + returnFiber : Fiber, + newIdx : number, + newChild : any, + priority : PriorityLevel + ) : ?Fiber { + + 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); } - switch (newChildren.$$typeof) { - case REACT_ELEMENT_TYPE: { - /* $FlowFixMe(>=0.31.0): This is an unsafe cast. Consider adding a type - * annotation to the `newChildren` param of this - * function. - */ - const element = (newChildren : ReactElement); - 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; + 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: { + const matchedFiber = existingChildren.get( + newChild.key === null ? newIdx : newChild.key + ) || null; + return updateYield(returnFiber, matchedFiber, newChild, priority); } - const child = createFiberFromElement(element, priority); - child.return = returnFiber; - return child; } - case REACT_COROUTINE_TYPE: { - /* $FlowFixMe(>=0.31.0): No 'handler' property found in object type - */ - const coroutine = (newChildren : ReactCoroutine); - const child = createFiberFromCoroutine(coroutine, priority); - child.return = returnFiber; - return child; + if (isArray(newChild) || getIteratorFn(newChild)) { + const matchedFiber = existingChildren.get(newIdx) || null; + return updateFragment(returnFiber, matchedFiber, newChild, priority); } + } - 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. - /* $FlowFixMe(>=0.31.0): No 'continuation' property found in object - * type - */ - const yieldNode = (newChildren : ReactYield); - const reifiedYield = createReifiedYield(yieldNode); - const child = createFiberFromYield(yieldNode, priority); - child.output = reifiedYield; - child.return = returnFiber; - return child; + 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 lastPlacedIndex = 0; + let newIdx = 0; + let nextOldFiber = null; + for (; oldFiber && newIdx < newChildren.length; newIdx++) { + if (oldFiber) { + if (oldFiber.index > newIdx) { + nextOldFiber = oldFiber; + oldFiber = null; + } else { + nextOldFiber = oldFiber.sibling; + } + } + const newFiber = updateSlot( + returnFiber, + oldFiber, + newChildren[newIdx], + priority + ); + if (!newFiber) { + // 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. + break; } + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + 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; } - if (isArray(newChildren)) { - var first : ?Fiber = null; - var prev : ?Fiber = null; - var existing : ?Fiber = existingChild; - /* $FlowIssue(>=0.31.0) #12747709 - * - * `Array.isArray` is matched syntactically for now until predicate - * support is complete. - */ - 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; + if (!oldFiber) { + // 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) { + // TODO: Move out of the loop. This only happens for the first run. + resultingFirstChild = newFiber; } else { - prev = createSubsequentChild(returnFiber, existing, prev, newChildren[i], priority); + 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) { + // 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 ? newFiber.index : newFiber.key + ); + } } - 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; + lastPlacedIndex = placeChild(newFiber, lastPlacedIndex, newIdx); + if (!previousNewFiber) { + resultingFirstChild = newFiber; + } else { + previousNewFiber.sibling = newFiber; } + previousNewFiber = newFiber; } - return first; - } else { - // TODO: Throw for unknown children. - return null; } + + 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, + 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; } - // TODO: This API won't work because we'll need to transfer the side-effects of - // unmounting children to the returnFiber. + function reconcileSingleElement( + returnFiber : Fiber, + currentFirstChild : ?Fiber, + element : ReactElement, + 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.ref = element.ref; + 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.ref = element.ref; + 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; + } + + // 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, - newChildren : ReactNodeList, + newChild : any, priority : PriorityLevel ) : ?Fiber { - return createFirstChild(returnFiber, currentFirstChild, newChildren, priority); - } + // 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. - return reconcileChildFibers; -} + if (typeof newChild === 'string' || typeof newChild === 'number') { + return placeSingleChild(reconcileSingleTextNode( + returnFiber, + currentFirstChild, + '' + newChild, + priority + )); + } + + if (typeof newChild === 'object' && newChild !== null) { + switch (newChild.$$typeof) { + case REACT_ELEMENT_TYPE: + return placeSingleChild(reconcileSingleElement( + returnFiber, + currentFirstChild, + newChild, + priority + )); -exports.reconcileChildFibers = ChildReconciler(true); + case REACT_COROUTINE_TYPE: + return placeSingleChild(reconcileSingleCoroutine( + returnFiber, + currentFirstChild, + newChild, + priority + )); -exports.reconcileChildFibersInPlace = ChildReconciler(false); + case REACT_YIELD_TYPE: + return placeSingleChild(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 + ); + } + } -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; + // Remaining cases are all treated as empty. + return deleteRemainingChildren(returnFiber, currentFirstChild); } - workInProgress.sibling = null; + + return reconcileChildFibers; } +exports.reconcileChildFibers = ChildReconciler(true, true); + +exports.reconcileChildFibersInPlace = ChildReconciler(false, true); + +exports.mountChildFibersInPlace = ChildReconciler(false, false); + exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) { if (!workInProgress.child) { return; @@ -242,7 +828,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 @@ -250,9 +836,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. diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index a733909551c3e..6c1d557325aaa 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -12,8 +12,10 @@ 'use strict'; +import type { ReactFragment } from 'ReactTypes'; import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { TypeOfWork } from 'ReactTypeOfWork'; +import type { TypeOfSideEffect } from 'ReactTypeOfSideEffect'; import type { PriorityLevel } from 'ReactPriorityLevel'; import type { UpdateQueue } from 'ReactFiberUpdateQueue'; @@ -23,14 +25,20 @@ var { ClassComponent, HostContainer, HostComponent, + HostText, CoroutineComponent, YieldComponent, + Fragment, } = ReactTypeOfWork; var { NoWork, } = require('ReactPriorityLevel'); +var { + NoEffect, +} = require('ReactTypeOfSideEffect'); + // An Instance is shared between all versions of a component. We can easily // break this out into a separate object to avoid copying so much to the // alternate versions of the tree. We put this on a single object for now to @@ -68,6 +76,7 @@ export type Fiber = Instance & { // Singly Linked List Tree Structure. child: ?Fiber, sibling: ?Fiber, + index: number, // The ref last used to attach this node. // I'll avoid adding an owner field for prod and model that as functions. @@ -87,6 +96,9 @@ export type Fiber = Instance & { // if this returns multiple values. Such as a fragment. output: any, // This type will be more specific once we overload the tag. + // Effect + effectTag: TypeOfSideEffect, + // Singly linked list fast path to the next fiber with side-effects. nextEffect: ?Fiber, @@ -110,6 +122,13 @@ export type Fiber = Instance & { // or may not be the same as the "current" child. progressedChild: ?Fiber, + // When we reconcile children onto progressedChild it is possible that we have + // to delete some child fibers. We need to keep track of this side-effects so + // that if we continue later on, we have to include those effects. Deletions + // are added in the reverse order from sibling pointers. + progressedFirstDeletion: ?Fiber, + progressedLastDeletion: ?Fiber, + // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save // memory if we need to. @@ -153,6 +172,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { child: null, sibling: null, + index: 0, ref: null, @@ -163,6 +183,7 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { callbackList: null, output: null, + effectTag: NoEffect, nextEffect: null, firstEffect: null, lastEffect: null, @@ -170,6 +191,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { pendingWorkPriority: NoWork, progressedPriority: NoWork, progressedChild: null, + progressedFirstDeletion: null, + progressedLastDeletion: null, alternate: null, @@ -198,8 +221,10 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi // extra memory if needed. let alt = fiber.alternate; if (alt) { - // Whenever we clone, we do so to get a new work in progress. - // This ensures that we've reset these in the new tree. + // If we clone, then we do so from the "current" state. The current state + // can't have any side-effects that are still valid so we reset just to be + // sure. + alt.effectTag = NoEffect; alt.nextEffect = null; alt.firstEffect = null; alt.lastEffect = null; @@ -218,6 +243,7 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi alt.stateNode = fiber.stateNode; alt.child = fiber.child; alt.sibling = fiber.sibling; // This should always be overridden. TODO: null + alt.index = fiber.index; // This should always be overridden. alt.ref = fiber.ref; // pendingProps is here for symmetry but is unnecessary in practice for now. // TODO: Pass in the new pendingProps as an argument maybe? @@ -245,6 +271,22 @@ exports.createFiberFromElement = function(element : ReactElement<*>, priorityLev return fiber; }; +exports.createFiberFromFragment = function(elements : ReactFragment, priorityLevel : PriorityLevel) { + // TODO: Consider supporting keyed fragments. Technically, we accidentally + // support that in the existing React. + const fiber = createFiber(Fragment, null); + fiber.pendingProps = elements; + fiber.pendingWorkPriority = priorityLevel; + return fiber; +}; + +exports.createFiberFromText = function(content : string, priorityLevel : PriorityLevel) { + const fiber = createFiber(HostText, null); + fiber.pendingProps = content; + fiber.pendingWorkPriority = priorityLevel; + return fiber; +}; + function createFiberFromElementType(type : mixed, key : null | string) { let fiber; if (typeof type === 'function') { @@ -257,6 +299,11 @@ function createFiberFromElementType(type : mixed, key : null | string) { fiber.type = type; } else if (typeof type === 'object' && type !== null) { // Currently assumed to be a continuation and therefore is a fiber already. + // TODO: The yield system is currently broken for updates in some cases. + // The reified yield stores a fiber, but we don't know which fiber that is; + // the current or a workInProgress? When the continuation gets rendered here + // we don't know if we can reuse that fiber or if we need to clone it. + // There is probably a clever way to restructure this. fiber = type; } else { throw new Error('Unknown component type: ' + typeof type); @@ -279,3 +326,4 @@ exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel : fiber.pendingProps = {}; return fiber; }; + diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 3d6266227c65d..c03d7097382e3 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -21,6 +21,7 @@ import type { PriorityLevel } from 'ReactPriorityLevel'; import type { UpdateQueue } from 'ReactFiberUpdateQueue'; var { + mountChildFibersInPlace, reconcileChildFibers, reconcileChildFibersInPlace, cloneChildFibers, @@ -33,9 +34,11 @@ var { ClassComponent, HostContainer, HostComponent, + HostText, CoroutineComponent, CoroutineHandlerPhase, YieldComponent, + Fragment, } = ReactTypeOfWork; var { NoWork, @@ -47,9 +50,12 @@ var { addCallbackToQueue, mergeUpdateQueue, } = require('ReactFiberUpdateQueue'); +var { + Placement, +} = require('ReactTypeOfSideEffect'); var ReactInstanceMap = require('ReactInstanceMap'); -module.exports = function(config : HostConfig, getScheduler : () => Scheduler) { +module.exports = function(config : HostConfig, getScheduler : () => Scheduler) { function markChildAsProgressed(current, workInProgress, priorityLevel) { // We now have clones. Let's store them as the currently progressed work. @@ -63,6 +69,18 @@ module.exports = function(config : HostConfig, getSchedu } } + function clearDeletions(workInProgress) { + workInProgress.progressedFirstDeletion = + workInProgress.progressedLastDeletion = + null; + } + + function transferDeletions(workInProgress) { + // Any deletions get added first into the effect list. + workInProgress.firstEffect = workInProgress.progressedFirstDeletion; + workInProgress.lastEffect = workInProgress.progressedLastDeletion; + } + function reconcileChildren(current, workInProgress, nextChildren) { const priorityLevel = workInProgress.pendingWorkPriority; reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel); @@ -72,31 +90,55 @@ module.exports = function(config : HostConfig, getSchedu // 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. + + // If we had any progressed work already, that is invalid at this point so + // let's throw it out. + clearDeletions(workInProgress); + workInProgress.child = reconcileChildFibers( workInProgress, workInProgress.child, nextChildren, priorityLevel ); + + transferDeletions(workInProgress); } else { - // If, on the other hand, we don't have a current fiber or if it is - // already using a clone, that means we've already begun some work on this - // tree and we can continue where we left off by reconciling against the - // existing children. + // If, on the other hand, it is already using a clone, that means we've + // already begun some work on this tree and we can continue where we left + // off by reconciling against the existing children. workInProgress.child = reconcileChildFibersInPlace( workInProgress, workInProgress.child, nextChildren, priorityLevel ); + + transferDeletions(workInProgress); } markChildAsProgressed(current, workInProgress, priorityLevel); } + function updateFragment(current, workInProgress) { + var nextChildren = workInProgress.pendingProps; + reconcileChildren(current, workInProgress, nextChildren); + } + function updateFunctionalComponent(current, workInProgress) { var fn = workInProgress.type; var props = workInProgress.pendingProps; @@ -233,7 +275,14 @@ module.exports = function(config : HostConfig, getSchedu } function updateHostComponent(current, workInProgress) { - const nextChildren = workInProgress.pendingProps.children; + let nextChildren = workInProgress.pendingProps.children; + if (typeof nextChildren === 'string' || typeof nextChildren === 'number') { + // We special case a direct text child of a host node. This is a common + // case. We won't handle it as a reified child. We will instead handle + // this in the host environment that also have access to this prop. That + // avoids allocating another HostText fiber and traversing it. + nextChildren = null; + } if (workInProgress.pendingProps.hidden && workInProgress.pendingWorkPriority !== OffscreenPriority) { // If this host component is hidden, we can bail out on the children. @@ -253,6 +302,20 @@ module.exports = function(config : HostConfig, getSchedu // Reconcile the children and stash them for later work. reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority); workInProgress.child = current ? current.child : null; + + if (!current) { + // If this doesn't have a current we won't track it for placement + // effects. However, when we come back around to this we have already + // inserted the parent which means that we'll infact need to make this a + // placement. + // TODO: There has to be a better solution to this problem. + let child = workInProgress.progressedChild; + while (child) { + child.effectTag = Placement; + child = child.sibling; + } + } + // Abort and don't process children yet. return null; } else { @@ -327,17 +390,20 @@ module.exports = function(config : HostConfig, getSchedu // return null; // } + if (current && workInProgress.child === current.child) { + // If we had any progressed work already, that is invalid at this point so + // let's throw it out. + clearDeletions(workInProgress); + } + cloneChildFibers(current, workInProgress); markChildAsProgressed(current, workInProgress, priorityLevel); return workInProgress.child; } function bailoutOnLowPriority(current, workInProgress) { - if (current) { - workInProgress.child = current.child; - workInProgress.memoizedProps = current.memoizedProps; - workInProgress.output = current.output; - } + // TODO: What if this is currently in progress? + // How can that happen? How is this not being cloned? return null; } @@ -347,6 +413,11 @@ module.exports = function(config : HostConfig, getSchedu return bailoutOnLowPriority(current, workInProgress); } + // If we don't bail out, we're going be recomputing our children so we need + // to drop our effect list. + workInProgress.firstEffect = null; + workInProgress.lastEffect = null; + if (workInProgress.progressedPriority === priorityLevel) { // If we have progressed work on this priority level already, we can // proceed this that as the child. @@ -372,19 +443,16 @@ module.exports = function(config : HostConfig, getSchedu reconcileChildren(current, workInProgress, workInProgress.pendingProps); // A yield component is just a placeholder, we can just run through the // next one immediately. - if (workInProgress.child) { - return beginWork( - workInProgress.child.alternate, - workInProgress.child, - priorityLevel - ); - } - return null; + return workInProgress.child; case HostComponent: if (workInProgress.stateNode && config.beginUpdate) { config.beginUpdate(workInProgress.stateNode); } return updateHostComponent(current, workInProgress); + case HostText: + // Nothing to do here. This is terminal. We'll do the completion step + // immediately after. + return null; case CoroutineHandlerPhase: // This is a restart. Reset the tag to the initial phase. workInProgress.tag = CoroutineComponent; @@ -393,25 +461,14 @@ module.exports = function(config : HostConfig, getSchedu updateCoroutineComponent(current, workInProgress); // This doesn't take arbitrary time so we could synchronously just begin // eagerly do the work of workInProgress.child as an optimization. - if (workInProgress.child) { - return beginWork( - workInProgress.child.alternate, - workInProgress.child, - priorityLevel - ); - } return workInProgress.child; case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. - if (workInProgress.sibling) { - return beginWork( - workInProgress.sibling.alternate, - workInProgress.sibling, - priorityLevel - ); - } return null; + case Fragment: + updateFragment(current, workInProgress); + return workInProgress.child; default: throw new Error('Unknown unit of work tag'); } diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index beb773a2e77c2..837f12d00c22e 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -21,30 +21,227 @@ var { ClassComponent, HostContainer, HostComponent, + HostText, } = ReactTypeOfWork; var { callCallbacks } = require('ReactFiberUpdateQueue'); -module.exports = function(config : HostConfig) { +var { + Placement, + PlacementAndUpdate, +} = require('ReactTypeOfSideEffect'); + +module.exports = function(config : HostConfig) { const updateContainer = config.updateContainer; const commitUpdate = config.commitUpdate; + const commitTextUpdate = config.commitTextUpdate; + + const appendChild = config.appendChild; + const insertBefore = config.insertBefore; + const removeChild = config.removeChild; + + function detachRef(current : Fiber) { + const ref = current.ref; + if (ref) { + ref(null); + } + } + + function detachRefIfNeeded(current : ?Fiber, finishedWork : Fiber) { + if (current) { + const currentRef = current.ref; + if (currentRef && currentRef !== finishedWork.ref) { + currentRef(null); + } + } + } + + function attachRef(current : ?Fiber, finishedWork : Fiber, instance : any) { + const ref = finishedWork.ref; + if (ref && (!current || current.ref !== ref)) { + ref(instance); + } + } + + function getHostParent(fiber : Fiber) : ?I { + let parent = fiber.return; + while (parent) { + switch (parent.tag) { + case HostComponent: + return parent.stateNode; + case HostContainer: + // TODO: Currently we use the updateContainer feature to update these, + // but we should be able to handle this case too. + return null; + } + parent = parent.return; + } + return null; + } + + function getHostSibling(fiber : Fiber) : ?I { + // We're going to search forward into the tree until we find a sibling host + // node. Unfortunately, if multiple insertions are done in a row we have to + // search past them. This leads to exponential search for the next sibling. + // TODO: Find a more efficient way to do this. + let node : Fiber = fiber; + siblings: while (true) { + // If we didn't find anything, let's try the next sibling. + while (!node.sibling) { + if (!node.return || node.return.tag === HostComponent) { + // If we pop out of the root or hit the parent the fiber we are the + // last sibling. + return null; + } + node = node.return; + } + node = node.sibling; + while (node.tag !== HostComponent && node.tag !== HostText) { + // If it is not host node and, we might have a host node inside it. + // Try to search down until we find one. + // TODO: For coroutines, this will have to search the stateNode. + if (node.effectTag === Placement || + node.effectTag === PlacementAndUpdate) { + // If we don't have a child, try the siblings instead. + continue siblings; + } + if (!node.child) { + continue siblings; + } else { + node = node.child; + } + } + // Check if this host node is stable or about to be placed. + if (node.effectTag !== Placement && + node.effectTag !== PlacementAndUpdate) { + // Found it! + return node.stateNode; + } + } + } + + function commitInsertion(finishedWork : Fiber) : void { + // Recursively insert all host nodes into the parent. + const parent = getHostParent(finishedWork); + if (!parent) { + return; + } + const before = getHostSibling(finishedWork); + // We only have the top Fiber that was inserted but we need recurse down its + // children to find all the terminal nodes. + let node : Fiber = finishedWork; + while (true) { + if (node.tag === HostComponent || node.tag === HostText) { + if (before) { + insertBefore(parent, node.stateNode, before); + } else { + appendChild(parent, node.stateNode); + } + } else if (node.child) { + // TODO: Coroutines need to visit the stateNode. + node = node.child; + continue; + } + if (node === finishedWork) { + return; + } + while (!node.sibling) { + if (!node.return || node.return === finishedWork) { + return; + } + node = node.return; + } + node = node.sibling; + } + } + + function commitNestedUnmounts(root : Fiber) { + // While we're inside a removed host node we don't want to call + // removeChild on the inner nodes because they're removed by the top + // call anyway. We also want to call componentWillUnmount on all + // composites before this host node is removed from the tree. Therefore + // we do an inner loop while we're still inside the host node. + let node : Fiber = root; + while (true) { + commitUnmount(node); + if (node.child) { + // TODO: Coroutines need to visit the stateNode. + node = node.child; + continue; + } + if (node === root) { + return; + } + while (!node.sibling) { + if (!node.return || node.return === root) { + return; + } + node = node.return; + } + node = node.sibling; + } + } + + function commitDeletion(current : Fiber) : void { + // Recursively delete all host nodes from the parent. + // TODO: Error handling. + const parent = getHostParent(current); + // We only have the top Fiber that was inserted but we need recurse down its + // children to find all the terminal nodes. + // TODO: Call componentWillUnmount on all classes as needed. Recurse down + // removed HostComponents but don't call removeChild on already removed + // children. + let node : Fiber = current; + while (true) { + if (node.tag === HostComponent || node.tag === HostText) { + commitNestedUnmounts(node); + // After all the children have unmounted, it is now safe to remove the + // node from the tree. + if (parent) { + removeChild(parent, node.stateNode); + } + } else { + commitUnmount(node); + if (node.child) { + // TODO: Coroutines need to visit the stateNode. + node = node.child; + continue; + } + } + if (node === current) { + return; + } + while (!node.sibling) { + if (!node.return || node.return === current) { + return; + } + node = node.return; + } + node = node.sibling; + } + } + + function commitUnmount(current : Fiber) : void { + switch (current.tag) { + case ClassComponent: { + detachRef(current); + const instance = current.stateNode; + if (typeof instance.componentWillUnmount === 'function') { + instance.componentWillUnmount(); + } + return; + } + case HostComponent: { + detachRef(current); + return; + } + } + } function commitWork(current : ?Fiber, finishedWork : Fiber) : void { switch (finishedWork.tag) { case ClassComponent: { - // Clear updates from current fiber. This must go before the callbacks - // are reset, in case an update is triggered from inside a callback. Is - // this safe? Relies on the assumption that work is only committed if - // the update queue is empty. - if (finishedWork.alternate) { - finishedWork.alternate.updateQueue = null; - } - if (finishedWork.callbackList) { - const { callbackList } = finishedWork; - finishedWork.callbackList = null; - callCallbacks(callbackList, finishedWork.stateNode); - } - // TODO: Fire componentDidMount/componentDidUpdate, update refs + detachRefIfNeeded(current, finishedWork); return; } case HostContainer: { @@ -56,16 +253,70 @@ module.exports = function(config : HostConfig) { return; } case HostComponent: { + const instance : I = finishedWork.stateNode; + if (instance != null && current) { + // Commit the work prepared earlier. + const newProps = finishedWork.memoizedProps; + const oldProps = current.memoizedProps; + commitUpdate(instance, oldProps, newProps); + } + detachRefIfNeeded(current, finishedWork); + return; + } + case HostText: { if (finishedWork.stateNode == null || !current) { throw new Error('This should only be done during updates.'); } - // Commit the work prepared earlier. - const child = finishedWork.child; - const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; - const newProps = finishedWork.memoizedProps; - const oldProps = current.memoizedProps; + const textInstance : TI = finishedWork.stateNode; + const newText : string = finishedWork.memoizedProps; + const oldText : string = current.memoizedProps; + commitTextUpdate(textInstance, oldText, newText); + return; + } + default: + throw new Error('This unit of work tag should not have side-effects.'); + } + } + + function commitLifeCycles(current : ?Fiber, finishedWork : Fiber) : void { + switch (finishedWork.tag) { + case ClassComponent: { + const instance = finishedWork.stateNode; + if (!current) { + if (typeof instance.componentDidMount === 'function') { + instance.componentDidMount(); + } + } else { + if (typeof instance.componentDidUpdate === 'function') { + const prevProps = current.memoizedProps; + // TODO: This is the new state. We don't currently have the previous + // state anymore. + const prevState = instance.state || null; + instance.componentDidUpdate(prevProps, prevState); + } + } + // Clear updates from current fiber. This must go before the callbacks + // are reset, in case an update is triggered from inside a callback. Is + // this safe? Relies on the assumption that work is only committed if + // the update queue is empty. + if (finishedWork.alternate) { + finishedWork.alternate.updateQueue = null; + } + if (finishedWork.callbackList) { + const { callbackList } = finishedWork; + finishedWork.callbackList = null; + callCallbacks(callbackList, instance); + } + attachRef(current, finishedWork, instance); + return; + } + case HostComponent: { const instance : I = finishedWork.stateNode; - commitUpdate(instance, oldProps, newProps, children); + attachRef(current, finishedWork, instance); + return; + } + case HostText: { + // We have no life-cycles associated with text. return; } default: @@ -74,7 +325,10 @@ module.exports = function(config : HostConfig) { } return { + commitInsertion, + commitDeletion, commitWork, + commitLifeCycles, }; }; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index d4687f457453a..4250f97bb0856 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -19,44 +19,33 @@ import type { ReifiedYield } from 'ReactReifiedYield'; var { reconcileChildFibers } = require('ReactChildFiber'); var ReactTypeOfWork = require('ReactTypeOfWork'); +var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var { IndeterminateComponent, FunctionalComponent, ClassComponent, HostContainer, HostComponent, + HostText, CoroutineComponent, CoroutineHandlerPhase, YieldComponent, + Fragment, } = ReactTypeOfWork; +var { + Update, +} = ReactTypeOfSideEffect; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { const createInstance = config.createInstance; + const createTextInstance = config.createTextInstance; const prepareUpdate = config.prepareUpdate; - function markForPreEffect(workInProgress : Fiber) { - // Schedule a side-effect on this fiber, BEFORE the children's side-effects. - if (workInProgress.firstEffect) { - workInProgress.nextEffect = workInProgress.firstEffect; - workInProgress.firstEffect = workInProgress; - } else { - workInProgress.firstEffect = workInProgress; - workInProgress.lastEffect = workInProgress; - } - } - - // TODO: It's possible this will create layout thrash issues because mutations - // of the DOM and life-cycles are interleaved. E.g. if a componentDidMount - // of a sibling reads, then the next sibling updates and reads etc. - function markForPostEffect(workInProgress : Fiber) { - // Schedule a side-effect on this fiber, AFTER the children's side-effects. - if (workInProgress.lastEffect) { - workInProgress.lastEffect.nextEffect = workInProgress; - } else { - workInProgress.firstEffect = workInProgress; - } - workInProgress.lastEffect = workInProgress; + function markUpdate(workInProgress : Fiber) { + // Tag the fiber with an update effect. This turns a Placement into + // an UpdateAndPlacement. + workInProgress.effectTag |= Update; } function transferOutput(child : ?Fiber, returnFiber : Fiber) { @@ -140,7 +129,7 @@ module.exports = function(config : HostConfig) { // Transfer update queue to callbackList field so callbacks can be // called during commit phase. workInProgress.callbackList = workInProgress.updateQueue; - markForPostEffect(workInProgress); + markUpdate(workInProgress); return null; case HostContainer: transferOutput(workInProgress.child, workInProgress); @@ -149,12 +138,10 @@ module.exports = function(config : HostConfig) { // all the other side-effects in the subtree. We need to schedule it // before so that the entire tree is up-to-date before the life-cycles // are invoked. - markForPreEffect(workInProgress); + markUpdate(workInProgress); return null; case HostComponent: let newProps = workInProgress.pendingProps; - const child = workInProgress.child; - const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; if (current && workInProgress.stateNode != null) { // If we have an alternate, that means this is an update and we need to // schedule a side-effect to do the updates. @@ -164,12 +151,12 @@ module.exports = function(config : HostConfig) { // TODO: Split the update API as separate for the props vs. children. // Even better would be if children weren't special cased at all tho. if (!newProps) { - newProps = oldProps; + newProps = workInProgress.memoizedProps || oldProps; } const instance : I = workInProgress.stateNode; - if (prepareUpdate(instance, oldProps, newProps, children)) { + if (prepareUpdate(instance, oldProps, newProps)) { // This returns true if there was something to update. - markForPreEffect(workInProgress); + markUpdate(workInProgress); } // TODO: Is this actually ever going to change? Why set it every time? workInProgress.output = instance; @@ -182,13 +169,41 @@ module.exports = function(config : HostConfig) { return null; } } + const child = workInProgress.child; + const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; const instance = createInstance(workInProgress.type, newProps, children); // TODO: This seems like unnecessary duplication. workInProgress.stateNode = instance; workInProgress.output = instance; + if (workInProgress.ref) { + // If there is a ref on a host node we need to schedule a callback + markUpdate(workInProgress); + } } workInProgress.memoizedProps = newProps; return null; + case HostText: + let newText = workInProgress.pendingProps; + if (current && workInProgress.stateNode != null) { + // If we have an alternate, that means this is an update and we need to + // schedule a side-effect to do the updates. + markUpdate(workInProgress); + } else { + if (typeof newText !== 'string') { + if (workInProgress.stateNode === null) { + throw new Error('We must have new props for new mounts.'); + } else { + // This can happen when we abort work. + return null; + } + } + const textInstance = createTextInstance(newText); + // TODO: This seems like unnecessary duplication. + workInProgress.stateNode = textInstance; + workInProgress.output = textInstance; + } + workInProgress.memoizedProps = newText; + return null; case CoroutineComponent: return moveCoroutineToHandlerPhase(current, workInProgress); case CoroutineHandlerPhase: @@ -199,6 +214,9 @@ module.exports = function(config : HostConfig) { case YieldComponent: // Does nothing. return null; + case Fragment: + transferOutput(workInProgress.child, workInProgress); + return null; // Error cases case IndeterminateComponent: diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index f0443a43ccee1..82a6175fd0c63 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -28,18 +28,24 @@ type HostChildNode = { tag: TypeOfWork, output: HostChildren, sibling: any export type HostChildren = null | void | I | HostChildNode; -export type HostConfig = { +export type HostConfig = { // TODO: We don't currently have a quick way to detect that children didn't // reorder so we host will always need to check the set. We should make a flag // or something so that it can bailout easily. - updateContainer(containerInfo : C, children : HostChildren) : void; + updateContainer(containerInfo : C, children : HostChildren) : void, - createInstance(type : T, props : P, children : HostChildren) : I, - prepareUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren) : boolean, - commitUpdate(instance : I, oldProps : P, newProps : P, children : HostChildren) : void, - deleteInstance(instance : I) : void, + createInstance(type : T, props : P, children : HostChildren) : I, + prepareUpdate(instance : I, oldProps : P, newProps : P) : boolean, + commitUpdate(instance : I, oldProps : P, newProps : P) : void, + + createTextInstance(text : string) : TI, + commitTextUpdate(textInstance : TI, oldText : string, newText : string) : void, + + appendChild(parentInstance : I, child : I | TI) : void, + insertBefore(parentInstance : I, child : I | TI, beforeChild : I | TI) : void, + removeChild(parentInstance : I, child : I | TI) : void, scheduleAnimationCallback(callback : () => void) : void, scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void @@ -58,7 +64,7 @@ export type Reconciler = { getPublicRootInstance(container : OpaqueNode) : (C | null), }; -module.exports = function(config : HostConfig) : Reconciler { +module.exports = function(config : HostConfig) : Reconciler { var { scheduleWork, performWithPriority } = ReactFiberScheduler(config); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 5bc128f5e5b9a..a1e0382ef77df 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -30,13 +30,21 @@ var { SynchronousPriority, } = require('ReactPriorityLevel'); +var { + NoEffect, + Placement, + Update, + PlacementAndUpdate, + Deletion, +} = require('ReactTypeOfSideEffect'); + var timeHeuristicForUnitOfWork = 1; export type Scheduler = { scheduleDeferredWork: (root : FiberRoot, priority : PriorityLevel) => void }; -module.exports = function(config : HostConfig) { +module.exports = function(config : HostConfig) { // Use a closure to circumvent the circular dependency between the scheduler // and ReactFiberBeginWork. Don't know if there's a better way to do this. let scheduler; @@ -46,7 +54,8 @@ module.exports = function(config : HostConfig) { const { beginWork } = ReactFiberBeginWork(config, getScheduler); const { completeWork } = ReactFiberCompleteWork(config); - const { commitWork } = ReactFiberCommitWork(config); + const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } = + ReactFiberCommitWork(config); const scheduleAnimationCallback = config.scheduleAnimationCallback; const scheduleDeferredCallback = config.scheduleDeferredCallback; @@ -103,10 +112,45 @@ module.exports = function(config : HostConfig) { function commitAllWork(finishedWork : Fiber) { // Commit all the side-effects within a tree. // TODO: Error handling. + + // First, we'll perform all the host insertions, updates, deletions and + // ref unmounts. let effectfulFiber = finishedWork.firstEffect; while (effectfulFiber) { - const current = effectfulFiber.alternate; - commitWork(current, effectfulFiber); + switch (effectfulFiber.effectTag) { + case Placement: { + commitInsertion(effectfulFiber); + break; + } + case PlacementAndUpdate: { + commitInsertion(effectfulFiber); + const current = effectfulFiber.alternate; + commitWork(current, effectfulFiber); + break; + } + case Update: { + const current = effectfulFiber.alternate; + commitWork(current, effectfulFiber); + break; + } + case Deletion: { + commitDeletion(effectfulFiber); + break; + } + } + effectfulFiber = effectfulFiber.nextEffect; + } + + // Next, we'll perform all life-cycles and ref callbacks. Life-cycles + // happens as a separate pass so that all effects in the entire tree have + // already been invoked. + effectfulFiber = finishedWork.firstEffect; + while (effectfulFiber) { + if (effectfulFiber.effectTag === Update || + effectfulFiber.effectTag === PlacementAndUpdate) { + const current = effectfulFiber.alternate; + commitLifeCycles(current, effectfulFiber); + } const next = effectfulFiber.nextEffect; // Ensure that we clean these up so that we don't accidentally keep them. // I'm not actually sure this matters because we can't reset firstEffect @@ -115,6 +159,13 @@ module.exports = function(config : HostConfig) { effectfulFiber.nextEffect = null; effectfulFiber = next; } + + // Finally if the root itself had an effect, we perform that since it is not + // part of the effect list. + if (finishedWork.effectTag !== NoEffect) { + const current = finishedWork.alternate; + commitWork(current, finishedWork); + } } function resetWorkPriority(workInProgress : Fiber) { @@ -154,9 +205,9 @@ module.exports = function(config : HostConfig) { const returnFiber = workInProgress.return; if (returnFiber) { - // Ensure that the first and last effect of the parent corresponds - // to the children's first and last effect. This probably relies on - // children completing in order. + // Append all the effects of the subtree and this fiber onto the effect + // list of the parent. The completion order of the children affects the + // side-effect order. if (!returnFiber.firstEffect) { returnFiber.firstEffect = workInProgress.firstEffect; } @@ -166,6 +217,21 @@ module.exports = function(config : HostConfig) { } returnFiber.lastEffect = workInProgress.lastEffect; } + + // If this fiber had side-effects, we append it AFTER the children's + // side-effects. We can perform certain side-effects earlier if + // needed, by doing multiple passes over the effect list. We don't want + // to schedule our own side-effect on our own list because if end up + // reusing children we'll schedule this effect onto itself since we're + // at the end. + if (workInProgress.effectTag !== NoEffect) { + if (returnFiber.lastEffect) { + returnFiber.lastEffect.nextEffect = workInProgress; + } else { + returnFiber.firstEffect = workInProgress; + } + returnFiber.lastEffect = workInProgress; + } } if (next) { diff --git a/src/renderers/shared/fiber/ReactReifiedYield.js b/src/renderers/shared/fiber/ReactReifiedYield.js index 41ef6b9218973..4fec70e180ce8 100644 --- a/src/renderers/shared/fiber/ReactReifiedYield.js +++ b/src/renderers/shared/fiber/ReactReifiedYield.js @@ -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, }; }; diff --git a/src/renderers/shared/fiber/ReactTypeOfSideEffect.js b/src/renderers/shared/fiber/ReactTypeOfSideEffect.js new file mode 100644 index 0000000000000..55a9cabc42e75 --- /dev/null +++ b/src/renderers/shared/fiber/ReactTypeOfSideEffect.js @@ -0,0 +1,23 @@ +/** + * 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 ReactTypeOfSideEffect + * @flow + */ + +'use strict'; + +export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4; + +module.exports = { + NoEffect: 0, + Placement: 1, + Update: 2, + PlacementAndUpdate: 3, + Deletion: 4, +}; diff --git a/src/renderers/shared/fiber/ReactTypeOfWork.js b/src/renderers/shared/fiber/ReactTypeOfWork.js index b63813b1d5c42..97830a8aea291 100644 --- a/src/renderers/shared/fiber/ReactTypeOfWork.js +++ b/src/renderers/shared/fiber/ReactTypeOfWork.js @@ -12,7 +12,7 @@ 'use strict'; -export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7; +export type TypeOfWork = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9; module.exports = { IndeterminateComponent: 0, // Before we know whether it is functional or class @@ -20,7 +20,9 @@ module.exports = { ClassComponent: 2, HostContainer: 3, // Root of a host tree. Could be nested inside another node. HostComponent: 4, - CoroutineComponent: 5, - CoroutineHandlerPhase: 6, - YieldComponent: 7, + HostText: 5, + CoroutineComponent: 6, + CoroutineHandlerPhase: 7, + YieldComponent: 8, + Fragment: 9, }; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index f13573c91fdf9..6d4db53996b95 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -56,7 +56,7 @@ describe('ReactIncremental', () => { expect(fooCalled).toBe(false); expect(barCalled).toBe(false); // Do one step of work. - ReactNoop.flushDeferredPri(7); + ReactNoop.flushDeferredPri(7 + 5); expect(fooCalled).toBe(true); expect(barCalled).toBe(false); // Do the rest of the work. @@ -143,7 +143,7 @@ describe('ReactIncremental', () => { ReactNoop.render(); // Flush part of the work - ReactNoop.flushDeferredPri(20); + ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual(['Foo', 'Bar']); @@ -153,7 +153,7 @@ describe('ReactIncremental', () => { ReactNoop.render(); // Flush part of the new work - ReactNoop.flushDeferredPri(20); + ReactNoop.flushDeferredPri(20 + 5); expect(ops).toEqual(['Foo', 'Bar']); @@ -328,7 +328,7 @@ describe('ReactIncremental', () => { // We're now rendering an update that will bail out on updating middle. ReactNoop.render(); - ReactNoop.flushDeferredPri(45); + ReactNoop.flushDeferredPri(45 + 5); expect(ops).toEqual(['Foo', 'Bar', 'Bar']); @@ -395,7 +395,7 @@ describe('ReactIncremental', () => { // Init ReactNoop.render(); - ReactNoop.flushDeferredPri(52); + ReactNoop.flushDeferredPri(52 + 5); expect(ops).toEqual(['Foo', 'Bar', 'Tester', 'Bar']); @@ -417,6 +417,24 @@ describe('ReactIncremental', () => { // after them which is not correct. ReactNoop.flush(); expect(ops).toEqual(['Bar', 'Middle', 'Bar']); + + ops = []; + + // Let us try this again without fully finishing the first time. This will + // create a hanging subtree that is reconciling at the normal priority. + ReactNoop.render(); + ReactNoop.flushDeferredPri(40); + + expect(ops).toEqual(['Foo', 'Bar']); + + ops = []; + + // This update will create a tree that aborts that work and down-prioritizes + // it. If the priority levels aren't down-prioritized correctly this may + // abort rendering of the down-prioritized content. + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar', 'Bar']); }); it('can reuse work done after being preempted', () => { @@ -467,7 +485,7 @@ describe('ReactIncremental', () => { // Init ReactNoop.render(); - ReactNoop.flushDeferredPri(55 + 25); + ReactNoop.flushDeferredPri(55 + 25 + 5); // We only finish the higher priority work. So the low pri content // has not yet finished mounting. @@ -489,7 +507,7 @@ describe('ReactIncremental', () => { // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(); - ReactNoop.flushDeferredPri(30 + 25); + ReactNoop.flushDeferredPri(30 + 25 + 5); expect(ops).toEqual(['Foo', 'Bar']); @@ -583,7 +601,7 @@ describe('ReactIncremental', () => { ops = []; // The middle content is now pending rendering... - ReactNoop.flushDeferredPri(30 + 25); + ReactNoop.flushDeferredPri(30 + 25 + 5); expect(ops).toEqual(['Content', 'Middle', 'Bar']); // One more Middle left. ops = []; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index 901b55f0df1be..9982a078e80c1 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -21,6 +21,7 @@ describe('ReactIncrementalSideEffects', () => { }); function div(...children) { + children = children.map(c => typeof c === 'string' ? { text: c } : c); return { type: 'div', children, prop: undefined }; } @@ -57,6 +58,112 @@ describe('ReactIncrementalSideEffects', () => { }); + it('can update child nodes of a fragment', function() { + + function Bar(props) { + return {props.text}; + } + + function Foo(props) { + return ( +
+ + {props.text === 'World' ? [ + , +
, + ] : props.text === 'Hi' ? [ +
, + , + ] : null} + +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(span(), span('test')), + ]); + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(span(), span(), div(), span('test')), + ]); + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(span(), div(), span(), span('test')), + ]); + + }); + + it('can update child nodes rendering into text nodes', function() { + + function Bar(props) { + return props.text; + } + + function Foo(props) { + return ( +
+ + {props.text === 'World' ? [ + , + '!', + ] : null} +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div('Hello'), + ]); + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div('World', 'World', '!'), + ]); + + }); + + it('can deletes children either components, host or text', function() { + + function Bar(props) { + return ; + } + + function Foo(props) { + return ( +
+ {props.show ? [ +
, + Hello, + 'World', + ] : []} +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(div(), span('Hello'), 'World'), + ]); + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div(), + ]); + + }); + it('does not update child nodes if a flush is aborted', () => { function Bar(props) { @@ -273,7 +380,7 @@ describe('ReactIncrementalSideEffects', () => { it('can defer side-effects and resume them later on', function() { class Bar extends React.Component { shouldComponentUpdate(nextProps) { - return this.props.idx !== nextProps; + return this.props.idx !== nextProps.idx; } render() { return ; @@ -330,10 +437,11 @@ describe('ReactIncrementalSideEffects', () => { ) ), ]); - ReactNoop.flushDeferredPri(30); + ReactNoop.render(); + ReactNoop.flush(); expect(ReactNoop.root.children).toEqual([ div( - span(2), + span(3), div( // New numbers. span(1), @@ -349,6 +457,149 @@ describe('ReactIncrementalSideEffects', () => { expect(innerSpanA).toBe(innerSpanB); }); + it('can defer side-effects and reuse them later - complex', function() { + var ops = []; + + class Bar extends React.Component { + shouldComponentUpdate(nextProps) { + return this.props.idx !== nextProps.idx; + } + render() { + ops.push('Bar'); + return ; + } + } + class Baz extends React.Component { + shouldComponentUpdate(nextProps) { + return this.props.idx !== nextProps.idx; + } + render() { + ops.push('Baz'); + return [, ]; + } + } + function Foo(props) { + ops.push('Foo'); + return ( +
+ + +
+ ); + } + ReactNoop.render(); + ReactNoop.flushDeferredPri(65); + expect(ReactNoop.root.children).toEqual([ + div( + span(0), + div(/*the spans are down-prioritized and not rendered yet*/) + ), + ]); + + expect(ops).toEqual(['Foo', 'Baz', 'Bar']); + ops = []; + + ReactNoop.render(); + ReactNoop.flushDeferredPri(70); + expect(ReactNoop.root.children).toEqual([ + div( + span(1), + div(/*still not rendered yet*/) + ), + ]); + + expect(ops).toEqual(['Foo', 'Baz', 'Bar']); + ops = []; + + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div( + span(1), + div( + // Now we had enough time to finish the spans. + span(0), + span(0), + span(0), + span(0), + span(0), + span(0) + ) + ), + ]); + + expect(ops).toEqual(['Bar', 'Baz', 'Bar', 'Bar', 'Baz', 'Bar', 'Bar']); + ops = []; + + // Now we're going to update the index but we'll only let it finish half + // way through. + ReactNoop.render(); + ReactNoop.flushDeferredPri(95); + expect(ReactNoop.root.children).toEqual([ + div( + span(2), + div( + // Still same old numbers. + span(0), + span(0), + span(0), + span(0), + span(0), + span(0) + ) + ), + ]); + + // We let it finish half way through. That means we'll have one fully + // completed Baz, one half-way completed Baz and one fully incomplete Baz. + expect(ops).toEqual(['Foo', 'Baz', 'Bar', 'Bar', 'Baz', 'Bar']); + ops = []; + + // We'll update again, without letting the new index update yet. Only half + // way through. + ReactNoop.render(); + ReactNoop.flushDeferredPri(50); + expect(ReactNoop.root.children).toEqual([ + div( + span(3), + div( + // Old numbers. + span(0), + span(0), + span(0), + span(0), + span(0), + span(0) + ) + ), + ]); + + expect(ops).toEqual(['Foo']); + ops = []; + + // We should now be able to reuse some of the work we've already done + // and replay those side-effects. + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + div( + span(3), + div( + // New numbers. + span(1), + span(1), + span(1), + span(1), + span(1), + span(1) + ) + ), + ]); + + expect(ops).toEqual(['Baz', 'Bar', 'Baz', 'Bar', 'Bar']); + }); // TODO: Test that side-effects are not cut off when a work in progress node // moves to "current" without flushing due to having lower priority. Does this @@ -384,4 +635,216 @@ describe('ReactIncrementalSideEffects', () => { }); // TODO: Test that callbacks are not lost if an update is preempted. + + it('calls componentWillUnmount after a deletion, even if nested', () => { + + var ops = []; + + class Bar extends React.Component { + componentWillUnmount() { + ops.push(this.props.name); + } + render() { + return ; + } + } + + class Wrapper extends React.Component { + componentWillUnmount() { + ops.push('Wrapper'); + } + render() { + return ; + } + } + + function Foo(props) { + return ( +
+ {props.show ? [ + , + , +
+ + , +
, + [ + , + , + ], + ] : []} +
+ {props.show ? : null} +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([]); + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + 'A', + 'Wrapper', + 'B', + 'C', + 'Wrapper', + 'D', + 'E', + 'F', + 'G', + ]); + + }); + + it('calls componentDidMount/Update after insertion/update', () => { + + var ops = []; + + class Bar extends React.Component { + componentDidMount() { + ops.push('mount:' + this.props.name); + } + componentDidUpdate() { + ops.push('update:' + this.props.name); + } + render() { + return ; + } + } + + class Wrapper extends React.Component { + componentDidMount() { + ops.push('mount:wrapper-' + this.props.name); + } + componentDidUpdate() { + ops.push('update:wrapper-' + this.props.name); + } + render() { + return ; + } + } + + function Foo(props) { + return ( +
+ + +
+ + +
+ {[ + , + , + ]} +
+ +
+
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + 'mount:A', + 'mount:B', + 'mount:wrapper-B', + 'mount:C', + 'mount:D', + 'mount:wrapper-D', + 'mount:E', + 'mount:F', + 'mount:G', + ]); + + ops = []; + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + 'update:A', + 'update:B', + 'update:wrapper-B', + 'update:C', + 'update:D', + 'update:wrapper-D', + 'update:E', + 'update:F', + 'update:G', + ]); + + }); + + it('invokes ref callbacks after insertion/update/unmount', () => { + + var classInstance = null; + + var ops = []; + + class ClassComponent extends React.Component { + render() { + classInstance = this; + return ; + } + } + + function FunctionalComponent(props) { + return ; + } + + function Foo(props) { + return ( + props.show ? +
+ ops.push(n)} /> + ops.push(n)} /> +
ops.push(n)} /> +
: + null + ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + classInstance, + // no call for functional components + div(), + ]); + + ops = []; + + // Refs that switch function instances get reinvoked + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + // detach all refs that switched handlers first. + null, + null, + // reattach as a separate phase + classInstance, + div(), + ]); + + ops = []; + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual([ + // unmount + null, + null, + ]); + + }); + + // TODO: Test that mounts, updates, refs, unmounts and deletions happen in the + // expected way for aborted and resumed render life-cycles. + }); diff --git a/src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js b/src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js new file mode 100644 index 0000000000000..18bea56c03ce5 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactTopLevelFragment-test.js @@ -0,0 +1,166 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +var React; +var ReactNoop; + +// This is a new feature in Fiber so I put it in its own test file. It could +// probably move to one of the other test files once it is official. +describe('ReactTopLevelFragment', function() { + beforeEach(function() { + React = require('React'); + ReactNoop = require('ReactNoop'); + }); + + it('should render a simple fragment at the top of a component', function() { + + function Fragment() { + return [
Hello
,
World
]; + } + ReactNoop.render(); + ReactNoop.flush(); + + }); + + it('should preserve state when switching from a single child', function() { + + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
Hello
; + } + } + + function Fragment({ condition }) { + return condition ? : + [,
World
]; + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).toBe(instanceA); + + }); + + it('should not preserve state when switching to a nested array', function() { + + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
Hello
; + } + } + + function Fragment({ condition }) { + return condition ? : + [[,
World
],
]; + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).not.toBe(instanceA); + + }); + + it('preserves state if an implicit key slot switches from/to null', function() { + + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
World
; + } + } + + function Fragment({ condition }) { + return condition ? [null, ] : + [
Hello
, ]; + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).toBe(instanceA); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceC = instance; + + expect(instanceC === instanceA).toBe(true); + + }); + + it('should preserve state in a reorder', function() { + + var instance = null; + + class Stateful extends React.Component { + render() { + instance = this; + return
Hello
; + } + } + + function Fragment({ condition }) { + return condition ? [[
World
, ]] : + [[,
World
],
]; + } + ReactNoop.render(); + ReactNoop.flush(); + + var instanceA = instance; + + expect(instanceA).not.toBe(null); + + ReactNoop.render(); + ReactNoop.flush(); + + var instanceB = instance; + + expect(instanceB).toBe(instanceA); + + }); + +}); diff --git a/src/renderers/shared/fiber/isomorphic/ReactCoroutine.js b/src/renderers/shared/fiber/isomorphic/ReactCoroutine.js index dda4ca1fb3c8e..609470fd39bc9 100644 --- a/src/renderers/shared/fiber/isomorphic/ReactCoroutine.js +++ b/src/renderers/shared/fiber/isomorphic/ReactCoroutine.js @@ -33,10 +33,7 @@ export type ReactCoroutine = { children: any, // This should be a more specific CoroutineHandler handler: (props: any, yields: Array) => ReactNodeList, - /* $FlowFixMe(>=0.31.0): Which is it? mixed? Or Object? Must match - * `ReactYield` type. - */ - props: mixed, + props: any, }; export type ReactYield = { $$typeof: Symbol | number, diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildReconcile-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildReconcile-test.js index d655536ae8fb4..43977bff1a6f9 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildReconcile-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildReconcile-test.js @@ -13,13 +13,10 @@ var React = require('React'); var ReactDOM = require('ReactDOM'); -var ReactDOMComponentTree = require('ReactDOMComponentTree'); -var ReactInstanceMap = require('ReactInstanceMap'); var stripEmptyValues = function(obj) { var ret = {}; - var name; - for (name in obj) { + for (var name in obj) { if (!obj.hasOwnProperty(name)) { continue; } @@ -30,15 +27,7 @@ var stripEmptyValues = function(obj) { return ret; }; -/** - * Child key names are wrapped like '.$key:0'. We strip the extra chars out - * here. This relies on an implementation detail of the rendering system. - */ -var getOriginalKey = function(childName) { - var match = childName.match(/^\.\$([^.]+)$/); - expect(match).not.toBeNull(); - return match[1]; -}; +var idCounter = 123; /** * Contains internal static internal state in order to test that updates to @@ -46,15 +35,15 @@ var getOriginalKey = function(childName) { * reusing existing DOM/memory resources. */ class StatusDisplay extends React.Component { - state = {internalState: Math.random()}; + state = {internalState: idCounter++}; - getStatus = () => { + getStatus() { return this.props.status; - }; + } - getInternalState = () => { + getInternalState() { return this.state.internalState; - }; + } componentDidMount() { this.props.onFlush(); @@ -67,7 +56,7 @@ class StatusDisplay extends React.Component { render() { return (
- {this.state.internalState} + {this.props.contentKey}
); } @@ -82,39 +71,29 @@ class FriendsStatusDisplay extends React.Component { * Refs are not maintained in the rendered order, and neither is * `this._renderedChildren` (surprisingly). */ - getOriginalKeys = () => { + getOriginalKeys() { var originalKeys = []; - // TODO: Update this to a better test that doesn't rely so much on internal - // implementation details. - var statusDisplays = - ReactInstanceMap.get(this) - ._renderedComponent - ._renderedChildren; - var name; - for (name in statusDisplays) { - var child = statusDisplays[name]; - var isPresent = !!child; - if (isPresent) { - originalKeys[child._mountIndex] = getOriginalKey(name); + for (var key in this.props.usernameToStatus) { + if (this.props.usernameToStatus[key]) { + originalKeys.push(key); } } return originalKeys; - }; + } /** * Retrieves the rendered children in a nice format for comparing to the input * `this.props.usernameToStatus`. */ - getStatusDisplays = () => { + getStatusDisplays() { var res = {}; - var i; var originalKeys = this.getOriginalKeys(); - for (i = 0; i < originalKeys.length; i++) { + for (var i = 0; i < originalKeys.length; i++) { var key = originalKeys[i]; res[key] = this.refs[key]; } return res; - }; + } /** * Verifies that by the time a child is flushed, the refs that appeared @@ -123,10 +102,9 @@ class FriendsStatusDisplay extends React.Component { * but our internal layer API depends on this assumption. We need to change * it to be more declarative before making ref resolution indeterministic. */ - verifyPreviousRefsResolved = (flushedKey) => { - var i; + verifyPreviousRefsResolved(flushedKey) { var originalKeys = this.getOriginalKeys(); - for (i = 0; i < originalKeys.length; i++) { + for (var i = 0; i < originalKeys.length; i++) { var key = originalKeys[i]; if (key === flushedKey) { // We are only interested in children up to the current key. @@ -134,18 +112,18 @@ class FriendsStatusDisplay extends React.Component { } expect(this.refs[key]).toBeTruthy(); } - }; + } render() { var children = []; - var key; - for (key in this.props.usernameToStatus) { + for (var key in this.props.usernameToStatus) { var status = this.props.usernameToStatus[key]; children.push( !status ? null : @@ -223,35 +201,33 @@ function verifyStatesPreserved(lastInternalStates, statusDisplays) { * Verifies that the internal representation of a set of `renderedChildren` * accurately reflects what is in the DOM. */ -function verifyDomOrderingAccurate(parentInstance, statusDisplays) { - var containerNode = ReactDOM.findDOMNode(parentInstance); +function verifyDomOrderingAccurate(outerContainer, statusDisplays) { + var containerNode = outerContainer.firstChild; var statusDisplayNodes = containerNode.childNodes; - var i; - var orderedDomIDs = []; - for (i = 0; i < statusDisplayNodes.length; i++) { - var inst = ReactDOMComponentTree.getInstanceFromNode(statusDisplayNodes[i]); - orderedDomIDs.push(inst._rootNodeID); + var orderedDomKeys = []; + for (var i = 0; i < statusDisplayNodes.length; i++) { + var contentKey = statusDisplayNodes[i].textContent; + orderedDomKeys.push(contentKey); } - var orderedLogicalIDs = []; + var orderedLogicalKeys = []; var username; for (username in statusDisplays) { if (!statusDisplays.hasOwnProperty(username)) { continue; } var statusDisplay = statusDisplays[username]; - orderedLogicalIDs.push( - ReactInstanceMap.get(statusDisplay)._renderedComponent._rootNodeID + orderedLogicalKeys.push( + statusDisplay.props.contentKey ); } - expect(orderedDomIDs).toEqual(orderedLogicalIDs); + expect(orderedDomKeys).toEqual(orderedLogicalKeys); } /** * Todo: Check that internal state is preserved across transitions */ function testPropsSequence(sequence) { - var i; var container = document.createElement('div'); var parentInstance = ReactDOM.render( , @@ -261,7 +237,7 @@ function testPropsSequence(sequence) { var lastInternalStates = getInternalStateByUserName(statusDisplays); verifyStatuses(statusDisplays, sequence[0]); - for (i = 1; i < sequence.length; i++) { + for (var i = 1; i < sequence.length; i++) { ReactDOM.render( , container @@ -269,7 +245,7 @@ function testPropsSequence(sequence) { statusDisplays = parentInstance.getStatusDisplays(); verifyStatuses(statusDisplays, sequence[i]); verifyStatesPreserved(lastInternalStates, statusDisplays); - verifyDomOrderingAccurate(parentInstance, statusDisplays); + verifyDomOrderingAccurate(container, statusDisplays); lastInternalStates = getInternalStateByUserName(statusDisplays); } diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildText-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildText-test.js index 3db920e6bea1c..57d4331d591e6 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildText-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactMultiChildText-test.js @@ -26,17 +26,17 @@ var testAllPermutations = function(testCases) { var expectedResultAfterUpdate = testCases[j + 1]; var container = document.createElement('div'); - var d = ReactDOM.render(
{renderWithChildren}
, container); - expectChildren(d, expectedResultAfterRender); + ReactDOM.render(
{renderWithChildren}
, container); + expectChildren(container, expectedResultAfterRender); - d = ReactDOM.render(
{updateWithChildren}
, container); - expectChildren(d, expectedResultAfterUpdate); + ReactDOM.render(
{updateWithChildren}
, container); + expectChildren(container, expectedResultAfterUpdate); } } }; -var expectChildren = function(d, children) { - var outerNode = ReactDOM.findDOMNode(d); +var expectChildren = function(container, children) { + var outerNode = container.firstChild; var textNode; if (typeof children === 'string') { textNode = outerNode.firstChild;