diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html index d2541eee1e93..0458a4c94fbc 100644 --- a/fixtures/fiber-triangle/index.html +++ b/fixtures/fiber-triangle/index.html @@ -26,6 +26,7 @@

Fiber Example

font: 'normal 15px sans-serif', textAlign: 'center', cursor: 'pointer', + color: 'white', }; var containerStyle = { @@ -40,6 +41,10 @@

Fiber Example

var targetSize = 25; + let dotCount = 0; + + const colors = ['red', 'blue', 'green', 'orange', 'purple', 'black']; + class Dot extends React.Component { constructor() { super(); @@ -66,11 +71,12 @@

Fiber Example

top: (props.y) + 'px', borderRadius: (s / 2) + 'px', lineHeight: (s) + 'px', - background: this.state.hover ? '#ff0' : dotStyle.background + background: this.state.hover ? '#ff0' : colors[props.text % colors.length], + color: this.state.hover ? 'black' : dotStyle.color, }; return ( -
this.enter()} onMouseLeave={() => this.leave()}> - {this.state.hover ? '*' + props.text + '*' : props.text} +
this.div = div} style={style} onMouseEnter={() => this.enter()} onMouseLeave={() => this.leave()}> + {this.state.hover ? `*${props.text}*` : props.text}
); } diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 789af77694f0..52415452247c 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -3,3 +3,6 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js * should not blow away user-entered text on successful reconnect to a controlled checkbox * should not blow away user-selected value on successful reconnect to an uncontrolled select * should not blow away user-selected value on successful reconnect to an controlled select + +src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js +* resumes work by comparing the priority at which the work-in-progress was created/updated diff --git a/scripts/fiber/tests-passing-except-dev.txt b/scripts/fiber/tests-passing-except-dev.txt index 4ae14d31d6c9..c91cd76d319c 100644 --- a/scripts/fiber/tests-passing-except-dev.txt +++ b/scripts/fiber/tests-passing-except-dev.txt @@ -86,12 +86,6 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js * renders a div with a single child surrounded by whitespace with client render on top of bad server markup * renders >,<, and & as single child with client render on top of bad server markup * renders >,<, and & as multiple children with client render on top of bad server markup -* throws when rendering a string component with clean client render -* throws when rendering a string component with client render on top of bad server markup -* throws when rendering an undefined component with clean client render -* throws when rendering an undefined component with client render on top of bad server markup -* throws when rendering a number component with clean client render -* throws when rendering a number component with client render on top of bad server markup * renders an input with a value and an onChange with client render on top of bad server markup * renders an input with a value and readOnly with client render on top of bad server markup * renders an input with a value and no onChange/readOnly with client render on top of bad server markup diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 930fb89124d8..f07e7eb25840 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -1121,8 +1121,14 @@ src/renderers/dom/shared/__tests__/ReactDOMServerIntegration-test.js * renders >,<, and & as multiple children with clean client render * renders >,<, and & as multiple children with client render on top of good server markup * throws when rendering a string component with server string render +* throws when rendering a string component with clean client render +* throws when rendering a string component with client render on top of bad server markup * throws when rendering an undefined component with server string render +* throws when rendering an undefined component with clean client render +* throws when rendering an undefined component with client render on top of bad server markup * throws when rendering a number component with server string render +* throws when rendering a number component with clean client render +* throws when rendering a number component with client render on top of bad server markup * throws when rendering null with server string render * throws when rendering null with clean client render * throws when rendering null with client render on top of bad server markup @@ -1860,6 +1866,8 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js * can defer side-effects and resume them later on * can defer side-effects and reuse them later - complex * deprioritizes setStates that happens within a deprioritized tree +* does not drop priority from a progressed subtree +* does not complete already completed work * calls callback after update is flushed * calls setState callback even if component bails out * calls componentWillUnmount after a deletion, even if nested @@ -1867,6 +1875,9 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js * invokes ref callbacks after insertion/update/unmount * supports string refs +src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js +* works + src/renderers/shared/fiber/__tests__/ReactIncrementalUpdates-test.js * applies updates in order of priority * applies updates with equal priority in insertion order diff --git a/src/renderers/__tests__/ReactComponent-test.js b/src/renderers/__tests__/ReactComponent-test.js index 1ea4d6dddce5..9f330f7b0827 100644 --- a/src/renderers/__tests__/ReactComponent-test.js +++ b/src/renderers/__tests__/ReactComponent-test.js @@ -44,7 +44,12 @@ describe('ReactComponent', () => { var instance =
; expect(function() { instance = ReactTestUtils.renderIntoDocument(instance); - }).toThrow(); + }).toThrow( + 'Only a ReactOwner can have refs. You might be adding a ref to a ' + + "component that was not created inside a component's `render` " + + 'method, or you have multiple copies of React loaded (details: ' + + 'https://fb.me/react-refs-must-have-owner).', + ); }); it('should warn when children are mutated during render', () => { diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index b17a2d40f6d2..95f8be45e3e8 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -115,7 +115,9 @@ function getReactRootElementInContainer(container: any) { function shouldReuseContent(container) { const rootElement = getReactRootElementInContainer(container); - return !!(rootElement && rootElement.getAttribute(ID_ATTRIBUTE_NAME)); + return !!(rootElement && + rootElement.nodeType === ELEMENT_NODE && + rootElement.getAttribute(ID_ATTRIBUTE_NAME)); } function shouldAutoFocusHostComponent(type: string, props: Props): boolean { diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 815b3d9748e8..b15127494ae9 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -212,6 +212,40 @@ var rootContainers = new Map(); var roots = new Map(); var DEFAULT_ROOT_ID = ''; +let yieldBeforeNextUnitOfWork = false; +let yieldValue = null; + +function* flushUnitsOfWork(n: number): Generator { + var didStop = false; + while (!didStop && scheduledDeferredCallback !== null) { + var cb = scheduledDeferredCallback; + scheduledDeferredCallback = null; + yieldBeforeNextUnitOfWork = false; + yieldValue = null; + var unitsRemaining = n; + var didYield = false; + cb({ + timeRemaining() { + if (yieldBeforeNextUnitOfWork) { + didYield = true; + return 0; + } + if (unitsRemaining-- > 0) { + return 999; + } + didStop = true; + return 0; + }, + }); + + if (didYield) { + const valueToYield = yieldValue; + yieldValue = null; + yield valueToYield; + } + } +} + var ReactNoop = { getChildren(rootID: string = DEFAULT_ROOT_ID) { const container = rootContainers.get(rootID); @@ -277,22 +311,17 @@ var ReactNoop = { }, flushDeferredPri(timeout: number = Infinity) { - var cb = scheduledDeferredCallback; - if (cb === null) { - return; + // The legacy version of this function decremented the timeout before + // returning the new time. + // TODO: Convert tests to use flushUnitsOfWork or flushAndYield instead. + const n = timeout / 5 - 1; + const iterator = flushUnitsOfWork(n); + let value = iterator.next(); + while (!value.done) { + value = iterator.next(); } - scheduledDeferredCallback = null; - var timeRemaining = timeout; - cb({ - timeRemaining() { - // Simulate a fix amount of time progressing between each call. - timeRemaining -= 5; - if (timeRemaining < 0) { - timeRemaining = 0; - } - return timeRemaining; - }, - }); + // Don't flush animation priority in this legacy function. Some tests may + // still rely on this behavior. }, flush() { @@ -300,6 +329,30 @@ var ReactNoop = { ReactNoop.flushDeferredPri(); }, + *flushAndYield(unitsOfWork: number = Infinity): Generator { + for (const value of flushUnitsOfWork(unitsOfWork)) { + yield value; + } + ReactNoop.flushAnimationPri(); + }, + + flushUnitsOfWork(n: number) { + const iterator = flushUnitsOfWork(n); + let value = iterator.next(); + while (!value.done) { + value = iterator.next(); + } + // TODO: We should always flush animation priority after flushing normal/low + // priority. Move this to flushUnitsOfWork generator once tests + // are converted. + ReactNoop.flushAnimationPri(); + }, + + yield(value: mixed) { + yieldBeforeNextUnitOfWork = true; + yieldValue = value; + }, + performAnimationWork(fn: Function) { NoopRenderer.performWithPriority(AnimationPriority, fn); }, @@ -377,7 +430,7 @@ var ReactNoop = { if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); } - const childInProgress = fiber.progressedChild; + const childInProgress = fiber.progressedWork.child; if (childInProgress && childInProgress !== fiber.child) { log( ' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.progressedPriority, diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 3587750b2384..b6d616e09e56 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -16,7 +16,6 @@ import type {ReactElement} from 'ReactElementType'; import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; import type {ReactInstance} from 'ReactInstanceType'; -import type {PriorityLevel} from 'ReactPriorityLevel'; var REACT_ELEMENT_TYPE = require('ReactElementSymbol'); var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine'); @@ -26,9 +25,9 @@ var ReactFiber = require('ReactFiber'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var ReactTypeOfWork = require('ReactTypeOfWork'); -var emptyObject = require('fbjs/lib/emptyObject'); var getIteratorFn = require('getIteratorFn'); var invariant = require('fbjs/lib/invariant'); +var emptyObject = require('fbjs/lib/emptyObject'); var ReactFeatureFlags = require('ReactFeatureFlags'); if (__DEV__) { @@ -76,7 +75,7 @@ if (__DEV__) { } const { - cloneFiber, + createWorkInProgress, createFiberFromElement, createFiberFromFragment, createFiberFromText, @@ -100,50 +99,61 @@ const { const {NoEffect, Placement, Deletion} = ReactTypeOfSideEffect; function coerceRef(current: Fiber | null, element: ReactElement) { - let mixedRef = element.ref; + const mixedRef = element.ref; if (mixedRef !== null && typeof mixedRef !== 'function') { - if (element._owner) { - const owner: ?(Fiber | ReactInstance) = (element._owner: any); - let inst; - if (owner) { - if (typeof owner.tag === 'number') { - const ownerFiber = ((owner: any): Fiber); - invariant( - ownerFiber.tag === ClassComponent, - 'Stateless function components cannot have refs.', - ); - inst = ownerFiber.stateNode; - } else { - // Stack - inst = (owner: any).getPublicInstance(); - } - } - invariant( - inst, - 'Missing owner for string ref %s. This error is likely caused by a ' + - 'bug in React. Please file an issue.', - mixedRef, - ); - const stringRef = '' + mixedRef; - // Check if previous string ref matches new string ref - if ( - current !== null && - current.ref !== null && - current.ref._stringRef === stringRef - ) { - return current.ref; + const owner: ?(Fiber | ReactInstance) = (element._owner: any); + invariant( + owner != null, + 'Only a ReactOwner can have refs. You might be adding a ref to a ' + + "component that was not created inside a component's `render` " + + 'method, or you have multiple copies of React loaded (details: ' + + 'https://fb.me/react-refs-must-have-owner).', + ); + let inst; + if (owner) { + if (typeof owner.tag === 'number') { + const ownerFiber = ((owner: any): Fiber); + invariant( + ownerFiber.tag === ClassComponent, + 'Stateless function components cannot have refs.', + ); + inst = ownerFiber.stateNode; + } else { + // Stack + inst = (owner: any).getPublicInstance(); } - const ref = function(value) { - const refs = inst.refs === emptyObject ? (inst.refs = {}) : inst.refs; - if (value === null) { - delete refs[stringRef]; - } else { - refs[stringRef] = value; - } - }; - ref._stringRef = stringRef; - return ref; } + invariant( + inst, + 'Missing owner for string ref %s. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + mixedRef, + ); + const stringRef = '' + mixedRef; + // Check if previous string ref matches new string ref + if ( + current !== null && + current.ref !== null && + current.ref._stringRef === stringRef + ) { + return current.ref; + } + const ref = function(value) { + const refs = + // TODO: Comparison to emptyObject always fails. Don't know why. + // inst.refs !== emptyObject ? inst.refs : (inst.refs = {}); + typeof Object.isExtensible === 'function' && + Object.isExtensible(inst.refs) + ? inst.refs + : (inst.refs = {}); + if (value === null) { + delete refs[stringRef]; + } else { + refs[stringRef] = value; + } + }; + ref._stringRef = stringRef; + return ref; } return mixedRef; } @@ -178,6 +188,7 @@ function ChildReconciler(shouldClone, 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 @@ -187,13 +198,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } childToDelete = childToDelete.alternate; } + // Deletions are added in reversed order so we add it to the front. - const last = returnFiber.progressedLastDeletion; + const last = returnFiber.lastDeletion; if (last !== null) { last.nextEffect = childToDelete; - returnFiber.progressedLastDeletion = childToDelete; + returnFiber.lastDeletion = childToDelete; } else { - returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete; + returnFiber.firstDeletion = returnFiber.lastDeletion = childToDelete; } childToDelete.nextEffect = null; childToDelete.effectTag = Deletion; @@ -239,19 +251,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return existingChildren; } - function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber { + function useFiber(fiber: Fiber, pendingProps: mixed): Fiber { // We currently set sibling to null and index to 0 here because it is easy // to forget to do before returning it. E.g. for the single child case. if (shouldClone) { - const clone = cloneFiber(fiber, priority); + const clone = createWorkInProgress(fiber, pendingProps); 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.pendingProps = pendingProps; fiber.effectTag = NoEffect; fiber.index = 0; fiber.sibling = null; @@ -300,21 +309,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, textContent: string, - priority: PriorityLevel, ) { if (current === null || current.tag !== HostText) { // Insert const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); - existing.pendingProps = textContent; + const existing = useFiber(current, textContent); existing.return = returnFiber; return existing; } @@ -324,23 +330,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, element: ReactElement, - priority: PriorityLevel, ): Fiber { if (current === null || current.type !== element.type) { // Insert const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, ); created.ref = coerceRef(current, element); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, element.props); existing.ref = coerceRef(current, element); - existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; @@ -354,7 +357,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, ): Fiber { // TODO: Should this also compare handler to determine whether to reuse? if (current === null || current.tag !== CoroutineComponent) { @@ -362,14 +364,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); - existing.pendingProps = coroutine; + const existing = useFiber(current, coroutine); existing.return = returnFiber; return existing; } @@ -379,21 +379,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, ): Fiber { if (current === null || current.tag !== YieldComponent) { // Insert const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, ); created.type = yieldNode.value; created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, emptyObject); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -404,7 +402,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, ): Fiber { if ( current === null || @@ -416,14 +413,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); - existing.pendingProps = portal.children || []; + const existing = useFiber(current, portal.children || []); existing.return = returnFiber; return existing; } @@ -433,31 +428,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, fragment: Iterable<*>, - priority: PriorityLevel, ): Fiber { if (current === null || current.tag !== Fragment) { // Insert const created = createFiberFromFragment( fragment, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); - existing.pendingProps = fragment; + const existing = useFiber(current, fragment); existing.return = returnFiber; return existing; } } - function createChild( - returnFiber: Fiber, - newChild: any, - priority: PriorityLevel, - ): Fiber | null { + function createChild(returnFiber: Fiber, newChild: any): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys. If the previous node is implicitly keyed // we can continue to replace it without aborting even if it is not a text @@ -465,7 +453,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( '' + newChild, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -477,7 +464,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( newChild, returnFiber.internalContextTag, - priority, ); created.ref = coerceRef(null, newChild); created.return = returnFiber; @@ -488,7 +474,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( newChild, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -498,7 +483,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( newChild, returnFiber.internalContextTag, - priority, ); created.type = newChild.value; created.return = returnFiber; @@ -509,7 +493,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( newChild, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -520,7 +503,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromFragment( newChild, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -536,7 +518,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, - priority: PriorityLevel, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. @@ -549,14 +530,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateTextNode(returnFiber, oldFiber, '' + newChild, priority); + return updateTextNode(returnFiber, oldFiber, '' + newChild); } if (typeof newChild === 'object' && newChild !== null) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: { if (newChild.key === key) { - return updateElement(returnFiber, oldFiber, newChild, priority); + return updateElement(returnFiber, oldFiber, newChild); } else { return null; } @@ -564,7 +545,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_COROUTINE_TYPE: { if (newChild.key === key) { - return updateCoroutine(returnFiber, oldFiber, newChild, priority); + return updateCoroutine(returnFiber, oldFiber, newChild); } else { return null; } @@ -575,7 +556,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // we can continue to replace it without aborting even if it is not a // yield. if (key === null) { - return updateYield(returnFiber, oldFiber, newChild, priority); + return updateYield(returnFiber, oldFiber, newChild); } else { return null; } @@ -583,7 +564,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_PORTAL_TYPE: { if (newChild.key === key) { - return updatePortal(returnFiber, oldFiber, newChild, priority); + return updatePortal(returnFiber, oldFiber, newChild); } else { return null; } @@ -596,7 +577,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateFragment(returnFiber, oldFiber, newChild, priority); + return updateFragment(returnFiber, oldFiber, newChild); } throwOnInvalidObjectType(returnFiber, newChild); @@ -610,13 +591,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, newIdx: number, newChild: any, - priority: PriorityLevel, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys, so we neither have to check the old nor // new node for the key. If both are text nodes, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateTextNode(returnFiber, matchedFiber, '' + newChild, priority); + return updateTextNode(returnFiber, matchedFiber, '' + newChild); } if (typeof newChild === 'object' && newChild !== null) { @@ -626,7 +606,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateElement(returnFiber, matchedFiber, newChild, priority); + return updateElement(returnFiber, matchedFiber, newChild); } case REACT_COROUTINE_TYPE: { @@ -634,14 +614,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateCoroutine(returnFiber, matchedFiber, newChild, priority); + return updateCoroutine(returnFiber, matchedFiber, newChild); } case REACT_YIELD_TYPE: { // Yields doesn't have keys, so we neither have to check the old nor // new node for the key. If both are yields, they match. const matchedFiber = existingChildren.get(newIdx) || null; - return updateYield(returnFiber, matchedFiber, newChild, priority); + return updateYield(returnFiber, matchedFiber, newChild); } case REACT_PORTAL_TYPE: { @@ -649,13 +629,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updatePortal(returnFiber, matchedFiber, newChild, priority); + return updatePortal(returnFiber, matchedFiber, newChild); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; - return updateFragment(returnFiber, matchedFiber, newChild, priority); + return updateFragment(returnFiber, matchedFiber, newChild); } throwOnInvalidObjectType(returnFiber, newChild); @@ -713,7 +693,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, - priority: PriorityLevel, ): Fiber | null { // This algorithm can't optimize by searching from boths ends since we // don't have backpointers on fibers. I'm trying to see how far we can get @@ -757,12 +736,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { nextOldFiber = oldFiber.sibling; } - const newFiber = updateSlot( - returnFiber, - oldFiber, - newChildren[newIdx], - priority, - ); + const newFiber = updateSlot(returnFiber, oldFiber, newChildren[newIdx]); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need @@ -805,11 +779,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // 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, - ); + const newFiber = createChild(returnFiber, newChildren[newIdx]); if (!newFiber) { continue; } @@ -835,7 +805,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, newChildren[newIdx], - priority, ); if (newFiber) { if (shouldTrackSideEffects) { @@ -872,7 +841,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildrenIterable: Iterable<*>, - priority: PriorityLevel, ): Fiber | null { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. @@ -936,7 +904,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { nextOldFiber = oldFiber.sibling; } - const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority); + const newFiber = updateSlot(returnFiber, oldFiber, step.value); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's // unfortunate because it triggers the slow path all the time. We need @@ -979,7 +947,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // If we don't have any more existing children we can choose a fast path // since the rest will all be insertions. for (; !step.done; newIdx++, (step = newChildren.next())) { - const newFiber = createChild(returnFiber, step.value, priority); + const newFiber = createChild(returnFiber, step.value); if (newFiber === null) { continue; } @@ -1005,7 +973,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, step.value, - priority, ); if (newFiber !== null) { if (shouldTrackSideEffects) { @@ -1042,7 +1009,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, - priority: PriorityLevel, ): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. @@ -1050,8 +1016,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // 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; + const existing = useFiber(currentFirstChild, textContent); existing.return = returnFiber; return existing; } @@ -1061,7 +1026,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -1071,7 +1035,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, - priority: PriorityLevel, ): Fiber { const key = element.key; let child = currentFirstChild; @@ -1081,9 +1044,8 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.type === element.type) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, element.props); existing.ref = coerceRef(child, element); - existing.pendingProps = element.props; existing.return = returnFiber; if (__DEV__) { existing._debugSource = element._source; @@ -1103,7 +1065,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, ); created.ref = coerceRef(currentFirstChild, element); created.return = returnFiber; @@ -1114,7 +1075,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, ): Fiber { const key = coroutine.key; let child = currentFirstChild; @@ -1124,8 +1084,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.tag === CoroutineComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); - existing.pendingProps = coroutine; + const existing = useFiber(child, coroutine); existing.return = returnFiber; return existing; } else { @@ -1141,7 +1100,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -1151,14 +1109,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, ): Fiber { // There's no need to check for keys on yields since they're stateless. let child = currentFirstChild; if (child !== null) { if (child.tag === YieldComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, emptyObject); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -1170,7 +1127,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, ); created.type = yieldNode.value; created.return = returnFiber; @@ -1181,7 +1137,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, ): Fiber { const key = portal.key; let child = currentFirstChild; @@ -1195,8 +1150,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { child.stateNode.implementation === portal.implementation ) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); - existing.pendingProps = portal.children || []; + const existing = useFiber(child, portal.children || []); existing.return = returnFiber; return existing; } else { @@ -1212,7 +1166,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, ); created.return = returnFiber; return created; @@ -1225,7 +1178,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, - priority: PriorityLevel, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1234,8 +1186,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const disableNewFiberFeatures = ReactFeatureFlags.disableNewFiberFeatures; + if (newChild === null) { + // Fast path for null children + return deleteRemainingChildren(returnFiber, currentFirstChild); + } + // Handle object types - const isObject = typeof newChild === 'object' && newChild !== null; + const isObject = typeof newChild === 'object'; if (isObject) { // Support only the subset of return types that Stack supports. Treat // everything else as empty, but log a warning. @@ -1243,34 +1200,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( - reconcileSingleElement( - returnFiber, - currentFirstChild, - newChild, - priority, - ), + reconcileSingleElement(returnFiber, currentFirstChild, newChild), ); case REACT_PORTAL_TYPE: return placeSingleChild( - reconcileSinglePortal( - returnFiber, - currentFirstChild, - newChild, - priority, - ), + reconcileSinglePortal(returnFiber, currentFirstChild, newChild), ); } } else { switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( - reconcileSingleElement( - returnFiber, - currentFirstChild, - newChild, - priority, - ), + reconcileSingleElement(returnFiber, currentFirstChild, newChild), ); case REACT_COROUTINE_TYPE: @@ -1279,28 +1221,17 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, ), ); case REACT_YIELD_TYPE: return placeSingleChild( - reconcileSingleYield( - returnFiber, - currentFirstChild, - newChild, - priority, - ), + reconcileSingleYield(returnFiber, currentFirstChild, newChild), ); case REACT_PORTAL_TYPE: return placeSingleChild( - reconcileSinglePortal( - returnFiber, - currentFirstChild, - newChild, - priority, - ), + reconcileSinglePortal(returnFiber, currentFirstChild, newChild), ); } } @@ -1349,22 +1280,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (typeof newChild === 'string' || typeof newChild === 'number') { return placeSingleChild( - reconcileSingleTextNode( - returnFiber, - currentFirstChild, - '' + newChild, - priority, - ), + reconcileSingleTextNode(returnFiber, currentFirstChild, '' + newChild), ); } if (isArray(newChild)) { - return reconcileChildrenArray( - returnFiber, - currentFirstChild, - newChild, - priority, - ); + return reconcileChildrenArray(returnFiber, currentFirstChild, newChild); } if (getIteratorFn(newChild)) { @@ -1372,7 +1293,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, ); } @@ -1422,48 +1342,3 @@ exports.reconcileChildFibers = ChildReconciler(true, true); exports.reconcileChildFibersInPlace = ChildReconciler(false, true); exports.mountChildFibersInPlace = ChildReconciler(false, false); - -exports.cloneChildFibers = function( - current: Fiber | null, - workInProgress: Fiber, -): void { - if (!workInProgress.child) { - return; - } - if (current !== null && workInProgress.child === current.child) { - // We use workInProgress.child since that lets Flow know that it can't be - // null since we validated that already. However, as the line above suggests - // they're actually the same thing. - let currentChild = workInProgress.child; - // TODO: This used to reset the pending priority. Not sure if that is needed. - // workInProgress.pendingWorkPriority = current.pendingWorkPriority; - // TODO: The below priority used to be set to NoWork which would've - // dropped work. This is currently unobservable but will become - // observable when the first sibling has lower priority work remaining - // than the next sibling. At that point we should add tests that catches - // this. - let newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority); - workInProgress.child = newChild; - - newChild.return = workInProgress; - while (currentChild.sibling !== null) { - currentChild = currentChild.sibling; - newChild = newChild.sibling = cloneFiber( - currentChild, - currentChild.pendingWorkPriority, - ); - newChild.return = workInProgress; - } - newChild.sibling = null; - } else { - // If there is no alternate, then we don't need to clone the children. - // If the children of the alternate fiber is a different set, then we don't - // need to clone. We need to reset the return fiber though since we'll - // traverse down into them. - let child = workInProgress.child; - while (child !== null) { - child.return = workInProgress; - child = child.sibling; - } - } -}; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 1cebd90a03c1..d40e32276cbb 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -39,14 +39,13 @@ var { } = require('ReactTypeOfWork'); var {NoWork} = require('ReactPriorityLevel'); - +var {getUpdatePriority} = require('ReactFiberUpdateQueue'); var {NoContext} = require('ReactTypeOfInternalContext'); var {NoEffect} = require('ReactTypeOfSideEffect'); -var {cloneUpdateQueue} = require('ReactFiberUpdateQueue'); - var invariant = require('fbjs/lib/invariant'); +var emptyObject = require('fbjs/lib/emptyObject'); if (__DEV__) { var getComponentName = require('getComponentName'); @@ -124,7 +123,7 @@ export type Fiber = { // A queue of state updates and callbacks. updateQueue: UpdateQueue | null, - // The state used to create the output + // The state used to create the output. TODO: Move this to the update queue? memoizedState: any, // Bitfield that describes properties about the fiber and its subtree. E.g. @@ -146,6 +145,10 @@ export type Fiber = { // this fiber. firstEffect: Fiber | null, lastEffect: Fiber | null, + // Child deletion effects are separate so that they can be kept at the front + // of the list + firstDeletion: Fiber | null, + lastDeletion: Fiber | null, // This will be used to quickly determine if a subtree has no pending changes. pendingWorkPriority: PriorityLevel, @@ -154,19 +157,11 @@ export type Fiber = { // component. This indicates whether it is better to continue from the // progressed work or if it is better to continue from the current state. progressedPriority: PriorityLevel, - - // If work bails out on a Fiber that already had some work started at a lower - // priority, then we need to store the progressed work somewhere. This holds - // the started child set until we need to get back to working on it. It may - // or may not be the same as the "current" child. - progressedChild: Fiber | null, - - // 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 | null, - progressedLastDeletion: Fiber | null, + progressedWork: ProgressedWork, + // A pooled ProgressedWork object, allocated once per fiber pair. Should not + // be accessed outside of this module. + _pooledProgressedWork: null | ProgressedWork, + newestWork: 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 @@ -178,6 +173,26 @@ export type Fiber = { // to be the same as work in progress. }; +// If work bails out on a Fiber that already had some work started at a lower +// priority, then we need to store the progressed work somewhere. This holds +// the started child set until we need to get back to working on it, along +// with the corresponding props and state. +export type ProgressedWork = { + child: Fiber | null, + // When we reconcile children onto the progressed child 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. + firstDeletion: Fiber | null, + lastDeletion: Fiber | null, + + memoizedProps: any, + memoizedState: any, + updateQueue: UpdateQueue | null, + + pendingWorkPriority: PriorityLevel, +}; + if (__DEV__) { var debugCounter = 1; } @@ -232,16 +247,22 @@ var createFiber = function( nextEffect: null, firstEffect: null, lastEffect: null, + firstDeletion: null, + lastDeletion: null, pendingWorkPriority: NoWork, + // TODO: Express this circular reference properly + progressedWork: (null: any), + _pooledProgressedWork: null, progressedPriority: NoWork, - progressedChild: null, - progressedFirstDeletion: null, - progressedLastDeletion: null, + newestWork: (null: any), alternate: null, }; + fiber.progressedWork = fiber; + fiber.newestWork = fiber; + if (__DEV__) { fiber._debugID = debugCounter++; fiber._debugSource = null; @@ -259,67 +280,92 @@ function shouldConstruct(Component) { return !!(Component.prototype && Component.prototype.isReactComponent); } -// This is used to create an alternate fiber to do work on. -// TODO: Rename to createWorkInProgressFiber or something like that. -exports.cloneFiber = function( - fiber: Fiber, - priorityLevel: PriorityLevel, -): Fiber { - // We clone to get a work in progress. That means that this fiber is the - // current. To make it safe to reuse that fiber later on as work in progress - // we need to reset its work in progress flag now. We don't have an - // opportunity to do this earlier since we don't traverse the tree when - // the work in progress tree becomes the current tree. - // fiber.progressedPriority = NoWork; - // fiber.progressedChild = null; +exports.createProgressedWorkFork = function( + child: Fiber | null, + firstDeletion: Fiber | null, + lastDeletion: Fiber | null, + memoizedProps: any, + memoizedState: any, + updateQueue: UpdateQueue | null, + pendingWorkPriority: PriorityLevel, +): ProgressedWork { + // ProgressedWork is a subset of Fiber. We use a separate Flow type to prevent + // access of extra properties, but we use a whole Fiber for monomorphism. + const progressedWork = createFiber( + // The actual values here don't matter. + HostRoot, + null, + NoContext, + ); + progressedWork.child = child; + progressedWork.firstDeletion = firstDeletion; + progressedWork.lastDeletion = lastDeletion; + progressedWork.memoizedProps = memoizedProps; + progressedWork.memoizedState = memoizedState; + progressedWork.updateQueue = updateQueue; + progressedWork.pendingWorkPriority = pendingWorkPriority; + return progressedWork; +}; +// This is used to create an alternate fiber to do work on. It's called during +// the parent's begin phase, either during reconciliation or after the parent +// bails out. +exports.createWorkInProgress = function( + current: Fiber, + pendingProps: mixed, +): Fiber { // We use a double buffering pooling technique because we know that we'll only // ever need at most two versions of a tree. We pool the "other" unused node // that we're free to reuse. This is lazily created to avoid allocating extra // objects for things that are never updated. It also allow us to reclaim the // extra memory if needed. - let alt = fiber.alternate; - if (alt !== null) { - // 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; - } else { - // This should not have an alternate already - alt = createFiber(fiber.tag, fiber.key, fiber.internalContextTag); - alt.type = fiber.type; - - alt.progressedChild = fiber.progressedChild; - alt.progressedPriority = fiber.progressedPriority; + let workInProgress = current.alternate; + if (workInProgress === null) { + // No existing alternate. Create a new fiber. + workInProgress = createFiber( + current.tag, + current.key, + current.internalContextTag, + ); + workInProgress.type = current.type; + + workInProgress.progressedPriority = current.progressedPriority; + workInProgress._pooledProgressedWork = current._pooledProgressedWork; + workInProgress.progressedWork = current.progressedWork; + workInProgress.newestWork = current.newestWork; + + // Clone child from current. + workInProgress.child = current.child; + // The deletion list on current is no longer valid. + workInProgress.firstDeletion = null; + workInProgress.lastDeletion = null; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.memoizedState = current.memoizedState; + workInProgress.updateQueue = current.updateQueue; + + workInProgress.stateNode = current.stateNode; + if (__DEV__) { + workInProgress._debugID = current._debugID; + workInProgress._debugSource = current._debugSource; + workInProgress._debugOwner = current._debugOwner; + } - alt.alternate = fiber; - fiber.alternate = alt; + workInProgress.alternate = current; + current.alternate = workInProgress; } - 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? - alt.pendingProps = fiber.pendingProps; - cloneUpdateQueue(fiber, alt); - alt.pendingWorkPriority = priorityLevel; + // Only touch fields that are set by the parent, not fields that correspond to + // the child (memoizedProps, memoizedState, et al), which will be determined + // during its own begin phase. + workInProgress.pendingProps = pendingProps; - alt.memoizedProps = fiber.memoizedProps; - alt.memoizedState = fiber.memoizedState; + // These are overriden during reconcilation. + workInProgress.effectTag = NoEffect; + workInProgress.sibling = current.sibling; // This should always be overridden. TODO: null + workInProgress.index = current.index; // This should always be overridden. + workInProgress.ref = current.ref; - if (__DEV__) { - alt._debugID = fiber._debugID; - alt._debugSource = fiber._debugSource; - alt._debugOwner = fiber._debugOwner; - } - - return alt; + return workInProgress; }; exports.createHostRootFiber = function(): Fiber { @@ -330,7 +376,6 @@ exports.createHostRootFiber = function(): Fiber { exports.createFiberFromElement = function( element: ReactElement, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, ): Fiber { let owner = null; if (__DEV__) { @@ -344,7 +389,6 @@ exports.createFiberFromElement = function( owner, ); fiber.pendingProps = element.props; - fiber.pendingWorkPriority = priorityLevel; if (__DEV__) { fiber._debugSource = element._source; @@ -357,24 +401,20 @@ exports.createFiberFromElement = function( exports.createFiberFromFragment = function( elements: ReactFragment, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, ): Fiber { // TODO: Consider supporting keyed fragments. Technically, we accidentally // support that in the existing React. const fiber = createFiber(Fragment, null, internalContextTag); fiber.pendingProps = elements; - fiber.pendingWorkPriority = priorityLevel; return fiber; }; exports.createFiberFromText = function( content: string, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, ): Fiber { const fiber = createFiber(HostText, null, internalContextTag); fiber.pendingProps = content; - fiber.pendingWorkPriority = priorityLevel; return fiber; }; @@ -438,6 +478,7 @@ exports.createFiberFromElementType = createFiberFromElementType; exports.createFiberFromHostInstanceForDeletion = function(): Fiber { const fiber = createFiber(HostComponent, null, NoContext); + fiber.pendingProps = emptyObject; fiber.type = 'DELETED'; return fiber; }; @@ -445,7 +486,6 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber { exports.createFiberFromCoroutine = function( coroutine: ReactCoroutine, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, ): Fiber { const fiber = createFiber( CoroutineComponent, @@ -454,30 +494,101 @@ exports.createFiberFromCoroutine = function( ); fiber.type = coroutine.handler; fiber.pendingProps = coroutine; - fiber.pendingWorkPriority = priorityLevel; return fiber; }; exports.createFiberFromYield = function( yieldNode: ReactYield, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, ): Fiber { const fiber = createFiber(YieldComponent, null, internalContextTag); + fiber.pendingProps = emptyObject; return fiber; }; exports.createFiberFromPortal = function( portal: ReactPortal, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, ): Fiber { const fiber = createFiber(HostPortal, portal.key, internalContextTag); fiber.pendingProps = portal.children || []; - fiber.pendingWorkPriority = priorityLevel; fiber.stateNode = { containerInfo: portal.containerInfo, implementation: portal.implementation, }; return fiber; }; + +// TODO: The remaining functions don't really belong in this module. I just put +// them here until I figure out what to do with them. +function largerPriority(p1: PriorityLevel, p2: PriorityLevel): PriorityLevel { + return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2; +} +exports.largerPriority = largerPriority; + +function appendEffects(workInProgress, firstEffect, lastEffect) { + if (workInProgress.firstEffect === null) { + workInProgress.firstEffect = firstEffect; + } + if (lastEffect !== null) { + if (workInProgress.lastEffect !== null) { + workInProgress.lastEffect.nextEffect = firstEffect; + } + workInProgress.lastEffect = lastEffect; + } +} + +exports.getPriorityFromChildren = function( + workInProgress: Fiber, +): PriorityLevel { + let priority = NoWork; + let child = workInProgress.child; + while (child !== null) { + priority = largerPriority(priority, child.pendingWorkPriority); + priority = largerPriority(priority, getUpdatePriority(child)); + child = child.sibling; + } + return priority; +}; + +type EffectList = { + firstEffect: Fiber | null, + lastEffect: Fiber | null, +}; + +exports.transferEffectsToParent = function( + returnFiber: EffectList, + workInProgress: Fiber, +) { + // 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. + + // Append deletions first + appendEffects( + returnFiber, + workInProgress.firstDeletion, + workInProgress.lastDeletion, + ); + // Now append the rest of the effect list + appendEffects( + returnFiber, + workInProgress.firstEffect, + 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 !== null) { + returnFiber.lastEffect.nextEffect = workInProgress; + } else { + returnFiber.firstEffect = workInProgress; + } + returnFiber.lastEffect = workInProgress; + } +}; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index c042ce8e9263..bc90dee355c1 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -12,19 +12,24 @@ 'use strict'; +import type {Fiber, ProgressedWork} from 'ReactFiber'; +import type {FiberRoot} from 'ReactFiberRoot'; +import type {UpdateQueue} from 'ReactFiberUpdateQueue'; import type {ReactCoroutine} from 'ReactTypes'; -import type {Fiber} from 'ReactFiber'; import type {HostContext} from 'ReactFiberHostContext'; import type {HydrationContext} from 'ReactFiberHydrationContext'; -import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; +var { + createWorkInProgress, + createProgressedWorkFork, + transferEffectsToParent, +} = require('ReactFiber'); var { mountChildFibersInPlace, reconcileChildFibers, reconcileChildFibersInPlace, - cloneChildFibers, } = require('ReactChildFiber'); var {beginUpdateQueue} = require('ReactFiberUpdateQueue'); var ReactTypeOfWork = require('ReactTypeOfWork'); @@ -37,298 +42,533 @@ var { invalidateContextProvider, } = require('ReactFiberContext'); var { - IndeterminateComponent, - FunctionalComponent, - ClassComponent, HostRoot, + HostPortal, HostComponent, HostText, - HostPortal, + IndeterminateComponent, + FunctionalComponent, + ClassComponent, + Fragment, CoroutineComponent, CoroutineHandlerPhase, YieldComponent, - Fragment, } = ReactTypeOfWork; +var { + ClassUpdater, + validateClassInstance, + callClassInstanceMethod, +} = require('ReactFiberClassComponent'); var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); -var {Placement, ContentReset, Err, Ref} = require('ReactTypeOfSideEffect'); -var ReactFiberClassComponent = require('ReactFiberClassComponent'); +var { + Placement, + Update, + ContentReset, + Ref, + Err, + Callback, +} = require('ReactTypeOfSideEffect'); +var {AsyncUpdates} = require('ReactTypeOfInternalContext'); var {ReactCurrentOwner} = require('ReactGlobalSharedState'); +var ReactFeatureFlags = require('ReactFeatureFlags'); +var ReactInstanceMap = require('ReactInstanceMap'); var invariant = require('fbjs/lib/invariant'); +var shallowEqual = require('fbjs/lib/shallowEqual'); if (__DEV__) { var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); - var {cancelWorkTimer} = require('ReactDebugFiberPerf'); var warning = require('fbjs/lib/warning'); + var { + startPhaseTimer, + stopPhaseTimer, + cancelWorkTimer, + } = require('ReactDebugFiberPerf'); + var getComponentName = require('getComponentName'); var warnedAboutStatelessRefs = {}; } -module.exports = function( - config: HostConfig, - hostContext: HostContext, - hydrationContext: HydrationContext, - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, -) { - const { - shouldSetTextContent, - useSyncScheduling, - shouldDeprioritizeSubtree, - } = config; - - const {pushHostContext, pushHostContainer} = hostContext; +function bailoutHiddenChildren( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any | null, + nextState: mixed | null, + renderPriority: PriorityLevel, +): Fiber | null { + // We didn't reconcile, but before bailing out, we still need to override + // the priority of the children in case it's higher than + // OffscreenPriority. This can happen when we switch from visible to + // hidden, or if setState is called somewhere in the tree. + // TODO: It would be better if this tree got its correct priority set + // during scheduleUpdate instead because otherwise we'll start a higher + // priority reconciliation first before we can get down here. However, + // that is a bit tricky since workInProgress and current can have + // different "hidden" settings. + workInProgress.pendingWorkPriority = OffscreenPriority; + return bailout(current, workInProgress, nextProps, null, renderPriority); +} - const { - enterHydrationState, - resetHydrationState, - tryToClaimNextHydratableInstance, - } = hydrationContext; +function reconcileHiddenChildren( + current: Fiber | null, + workInProgress: Fiber, + nextChildren: any, + nextProps: any | null, + nextState: mixed | null, + renderPriority: PriorityLevel, +): Fiber | null { + if (renderPriority !== OffscreenPriority) { + // This is a special case where we're about to reconcile at a lower + // priority than the render priority. We already called resumeOrResetWork + // at the start of the begin phase, but we need to call it again with + // OffscreenPriority so that if we have an offscreen child, we can + // reuse it. + resumeOrResetWork(current, workInProgress, OffscreenPriority); + } - const { - adoptClassInstance, - constructClassInstance, - mountClassInstance, - resumeMountClassInstance, - updateClassInstance, - } = ReactFiberClassComponent( - scheduleUpdate, - getPriorityContext, - memoizeProps, - memoizeState, + // Reconcile the children at OffscreenPriority. This may be lower than + // the priority at which we're currently reconciling. This will store + // the children on the progressed work so that we can come back to them + // later if needed. + reconcile( + current, + workInProgress, + nextChildren, + nextProps, + nextState, + OffscreenPriority, ); - function markChildAsProgressed(current, workInProgress, priorityLevel) { - // We now have clones. Let's store them as the currently progressed work. - workInProgress.progressedChild = workInProgress.child; - workInProgress.progressedPriority = priorityLevel; - if (current !== null) { - // We also store it on the current. When the alternate swaps in we can - // continue from this point. - current.progressedChild = workInProgress.progressedChild; - current.progressedPriority = workInProgress.progressedPriority; + // If we're rendering at OffscreenPriority, start working on the child. + if (renderPriority === OffscreenPriority) { + return workInProgress.child; + } + + // Otherwise, bailout. + if (current === null) { + // 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.child; + while (child !== null) { + child.effectTag = Placement; + child = child.sibling; } } - function clearDeletions(workInProgress) { - workInProgress.progressedFirstDeletion = workInProgress.progressedLastDeletion = null; + // Usually we update the newestWork at the end of the begin phase. But in this + // case, we're doing a reconcile followed immediately by a bailout. So we + // need to update newestWork here so that it's correct by the time we bailout. + workInProgress.newestWork = workInProgress; + if (current !== null) { + workInProgress.newestWork = workInProgress; } - function transferDeletions(workInProgress) { - // Any deletions get added first into the effect list. - workInProgress.firstEffect = workInProgress.progressedFirstDeletion; - workInProgress.lastEffect = workInProgress.progressedLastDeletion; + resumeOrResetWork(current, workInProgress, renderPriority); + + // This will stash the child on a progressed work fork and reset to current. + bailout(current, workInProgress, nextProps, nextState, renderPriority); + + // Even though we're bailing out, we actually did complete the work at this + // priority. Update the memoized inputs so we can reuse it later. + // TODO: Is there a better way to model this? A bit confusing. Or maybe + // just a better explanation here would suffice. + workInProgress.memoizedProps = nextProps; + workInProgress.memoizedState = nextState; + + return null; +} + +function bailout( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any | null, + nextState: mixed | null, + renderPriority: PriorityLevel, +): Fiber | null { + if (__DEV__) { + cancelWorkTimer(workInProgress); } - function reconcileChildren(current, workInProgress, nextChildren) { - const priorityLevel = workInProgress.pendingWorkPriority; - reconcileChildrenAtPriority( - current, - workInProgress, - nextChildren, - priorityLevel, - ); + // A bailout implies that the memoized props and state are equal to the next + // props and state, but we should update them anyway because they might not + // be referentially equal (shouldComponentUpdate -> false) + workInProgress.memoizedProps = nextProps; + workInProgress.memoizedState = nextState; + + // If the child is null, this is terminal. The work is done. + if (workInProgress.child === null) { + return null; } - function reconcileChildrenAtPriority( + // Should we continue working on the children? Check if the children have + // work that matches the priority at which we're currently rendering. + if ( + workInProgress.pendingWorkPriority === NoWork || + workInProgress.pendingWorkPriority > renderPriority + ) { + // The children do not have sufficient priority. Return null to skip the + // children and continue on the sibling. If there's still work in the + // children, we'll come back to it later at a lower priority. + + if ( + current === null || + current.child === null || + workInProgress.child !== current.child + ) { + // We have progressed work that completed at this level. Because the + // remaining priority (pendingWorkPriority) is less than the priority + // at which it last rendered (progressedPriority), we know that it + // must have completed at the progressedPriority. That means we can + // use the progressed child during this commit. + + // We need to bubble up effects from the progressed children so that + // they don't get dropped. Usually effects are transferred to the + // parent during the complete phase, but we won't be completing these + // children again. + let child = workInProgress.child; + while (child !== null) { + transferEffectsToParent(workInProgress, child); + child = child.sibling; + } + } + return null; + } + + // The children have pending work that matches the render priority. Continue + // on the work-in-progress children. + if ( + current === null || + current.child === null || + workInProgress.child !== current.child + ) { + // The child is not the current child, which means they are a work-in- + // progress set. We can reuse them. But reset the child pointer before + // traversing into them so we can find our way back later. + let child = workInProgress.child; + while (child !== null) { + child.return = workInProgress; + child = child.sibling; + } + } else { + // The child is the current child. Switch to the work-in-progress set + // instead. If a child does not already have a work-in-progress copy, + // it will be created. + let currentChild = workInProgress.child; + let newChild = createWorkInProgress(currentChild, null); + workInProgress.child = newChild; + + newChild.return = workInProgress; + while (currentChild.sibling !== null) { + currentChild = currentChild.sibling; + newChild = newChild.sibling = createWorkInProgress(currentChild, null); + newChild.return = workInProgress; + } + newChild.sibling = null; + + // We mutated the child fiber. Mark it as progressed. If we had lower- + // priority progressed work, it will be thrown out. + markWorkAsProgressed(current, workInProgress, renderPriority); + } + + // Continue working on child + return workInProgress.child; +} +exports.bailout = bailout; + +function reconcile( + current: Fiber | null, + workInProgress: Fiber, + nextChildren: any, + nextProps: any | null, + nextState: mixed | null, + renderPriority: PriorityLevel, +) { + const child = (workInProgress.child = reconcileImpl( current, workInProgress, + workInProgress.child, nextChildren, - priorityLevel, - ) { - // At this point any memoization is no longer valid since we'll have changed - // the children. - workInProgress.memoizedProps = null; - if (current === null) { + nextProps, + nextState, + false, + renderPriority, + )); + return child; +} +exports.reconcile = reconcile; + +// Lower level version of reconcile function with more options for special +// cases. My thinking is that, for now, this is preferable to forking, because +// it's really easy to mess up when keeping the forks in sync. +// TODO: Unify this with reconcileChildFibers constructor? +function reconcileImpl( + current: Fiber | null, + workInProgress: Fiber, + child: Fiber | null, // Child to reconcile against + nextChildren: any, + nextProps: any | null, + nextState: mixed | null, + forceMountInPlace: boolean, + renderPriority: PriorityLevel, +): Fiber | null { + // We have new children. Update the child set. + let newChild; + if (forceMountInPlace) { + newChild = mountChildFibersInPlace(workInProgress, child, nextChildren); + } else if (current === null) { + if (workInProgress.tag === HostPortal) { + // Portals are special because we don't append the children during mount + // but at commit. Therefore we need to track insertions which the normal + // flow doesn't do during mount. This doesn't happen at the root because + // the root always starts with a "current" with a null child. + // TODO: Consider unifying this with how the root works. + newChild = reconcileChildFibersInPlace( + workInProgress, + child, + nextChildren, + ); + } else { // 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. + newChild = mountChildFibersInPlace(workInProgress, child, nextChildren); + } + } else if (workInProgress.child === current.child) { + // If the child is the same as the current child, 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. + // Note: Compare to `workInProgress.child`, not `child`, because for + // a phase one coroutine, `child` is actually the state node. + newChild = reconcileChildFibers(workInProgress, child, nextChildren); + } else { + // 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. + newChild = reconcileChildFibersInPlace(workInProgress, child, nextChildren); + } - // If we had any progressed work already, that is invalid at this point so - // let's throw it out. - clearDeletions(workInProgress); + // Memoize this work. + workInProgress.memoizedProps = nextProps; + workInProgress.memoizedState = nextState; - workInProgress.child = reconcileChildFibers( - workInProgress, - workInProgress.child, - nextChildren, - priorityLevel, - ); + // The child is now the progressed child. Update the progressed work. + markWorkAsProgressed(current, workInProgress, renderPriority); - transferDeletions(workInProgress); - } else { - // 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, - ); + // We reconciled the children set. They now have pending work at whatever + // priority we're currently rendering. This is true even if the render + // priority is less than the existing work priority, since that should only + // happen in the case of an intentional down-prioritization. + workInProgress.pendingWorkPriority = renderPriority; - transferDeletions(workInProgress); - } - markChildAsProgressed(current, workInProgress, priorityLevel); - } + return newChild; +} - function updateFragment(current, workInProgress) { - var nextChildren = workInProgress.pendingProps; - if (hasContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - if (nextChildren === null) { - nextChildren = workInProgress.memoizedProps; - } - } else if ( - nextChildren === null || - workInProgress.memoizedProps === nextChildren - ) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextChildren); - return workInProgress.child; +function markWorkAsProgressed(current, workInProgress, renderPriority) { + // Keep track of the priority at which this work was performed. + workInProgress.progressedPriority = renderPriority; + workInProgress.progressedWork = workInProgress; + if (current !== null) { + // Set the progressed work on both fibers + current.progressedPriority = renderPriority; + current.progressedWork = workInProgress; } +} - function markRef(current: Fiber | null, workInProgress: Fiber) { - const ref = workInProgress.ref; - if (ref !== null && (!current || current.ref !== ref)) { - // Schedule a Ref effect - workInProgress.effectTag |= Ref; - } +function resumeAlreadyProgressedWork( + workInProgress: Fiber, + progressedWork: ProgressedWork, +) { + // Reuse the progressed work. + if (progressedWork === workInProgress) { + // The work-in-progress is already the same as the progressed work. + return; } - function updateFunctionalComponent(current, workInProgress) { - var fn = workInProgress.type; - var nextProps = workInProgress.pendingProps; + // We're resuming off a fork, so we need to update the progressed work + // priority, too. The remaining work priority is equal to whatever priority + // we interrupted. + + workInProgress.child = progressedWork.child; + workInProgress.firstDeletion = progressedWork.firstDeletion; + workInProgress.lastDeletion = progressedWork.lastDeletion; + workInProgress.memoizedProps = progressedWork.memoizedProps; + workInProgress.memoizedState = progressedWork.memoizedState; + workInProgress.updateQueue = progressedWork.updateQueue; + workInProgress.pendingWorkPriority = progressedWork.pendingWorkPriority; + + // "Un-fork" the progressed work object. We no longer need it. + workInProgress.progressedWork = workInProgress; + const current = workInProgress.alternate; + if (current !== null) { + current.progressedWork = workInProgress; + } +} - const memoizedProps = workInProgress.memoizedProps; - if (hasContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - if (nextProps === null) { - nextProps = memoizedProps; - } +function getWorkProgressedSinceLastCommit( + current: Fiber | null, + workInProgress: Fiber, +): ProgressedWork | null { + const progressedWork = workInProgress.progressedWork; + if (progressedWork === workInProgress) { + if (workInProgress === workInProgress.newestWork) { + // The work-in-progress fiber has work that is newer than the + // current fiber. + return workInProgress; } else { - if (nextProps === null || memoizedProps === nextProps) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - // TODO: Disable this before release, since it is not part of the public API - // I use this for testing to compare the relative overhead of classes. - if ( - typeof fn.shouldComponentUpdate === 'function' && - !fn.shouldComponentUpdate(memoizedProps, nextProps) - ) { - // Memoize props even if shouldComponentUpdate returns false - memoizeProps(workInProgress, nextProps); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } + // The work-in-progress is older than the current fiber. In other words, + // not a true work-in-progress, but the "previous current" fiber. + return null; } + } else if (progressedWork === current) { + // There's been no work since the last commit. + return null; + } else { + // We have stashed work. + return progressedWork; + } +} - var unmaskedContext = getUnmaskedContext(workInProgress); - var context = getMaskedContext(workInProgress, unmaskedContext); - - var nextChildren; - - if (__DEV__) { - ReactCurrentOwner.current = workInProgress; - ReactDebugCurrentFiber.phase = 'render'; - nextChildren = fn(nextProps, context); - ReactDebugCurrentFiber.phase = null; - } else { - nextChildren = fn(nextProps, context); - } - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextProps); - return workInProgress.child; +function forkWorkInProgress( + current: Fiber | null, + workInProgress: Fiber, + child: Fiber | null, + firstDeletion: Fiber | null, + lastDeletion: Fiber | null, + memoizedProps: any, + memoizedState: any, + updateQueue: UpdateQueue | null, + pendingWorkPriority: PriorityLevel, +) { + const stashedWork = createProgressedWorkFork( + child, + firstDeletion, + lastDeletion, + memoizedProps, + memoizedState, + updateQueue, + pendingWorkPriority, + ); + workInProgress.progressedWork = stashedWork; + if (current !== null) { + // Set it on both fibers + current.progressedWork = stashedWork; } +} - function updateClassComponent( - current: Fiber | null, - workInProgress: Fiber, - priorityLevel: PriorityLevel, - ) { - // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - const hasContext = pushContextProvider(workInProgress); +function resetToCurrent( + current: Fiber | null, + workInProgress: Fiber, + renderPriority, +): void { + if (current !== null) { + // Clone child from current, along with associated fields. + workInProgress.child = current.child; + // The deletion list on current is no longer valid. + workInProgress.firstDeletion = null; + workInProgress.lastDeletion = null; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.memoizedState = current.memoizedState; + workInProgress.updateQueue = current.updateQueue; + workInProgress.pendingWorkPriority = current.pendingWorkPriority; + + // The effect list can be left alone because we reset it at the start of + // the begin phase. Otherwise we'd reset it to null here, since effects + // on the current tree are no longer valid. + + // The remaining fields belong to the "instance" and are always kept in + // sync on both copies of the fiber, so we don't need to copy them here. + } else { + // There is no current, so conceptually, the current fiber is null. + workInProgress.child = null; + workInProgress.firstDeletion = null; + workInProgress.lastDeletion = null; + workInProgress.memoizedProps = null; + workInProgress.memoizedState = null; + workInProgress.updateQueue = null; + workInProgress.pendingWorkPriority = NoWork; + } +} - let shouldUpdate; - if (current === null) { - if (!workInProgress.stateNode) { - // In the initial pass we might need to construct the instance. - constructClassInstance(workInProgress, workInProgress.pendingProps); - mountClassInstance(workInProgress, priorityLevel); - shouldUpdate = true; - } else { - // In a resume, we'll already have an instance we can reuse. - shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel); - } +function resumeOrResetWork( + current: Fiber | null, + workInProgress: Fiber, + renderPriority: PriorityLevel, +): void { + // See if there's any work that has progressed since the last commit + const progressedWork = getWorkProgressedSinceLastCommit( + current, + workInProgress, + ); + if (progressedWork !== null) { + // Check to make sure the work progressed at the same priority + if (workInProgress.progressedPriority === renderPriority) { + resumeAlreadyProgressedWork(workInProgress, progressedWork); } else { - shouldUpdate = updateClassInstance( - current, - workInProgress, - priorityLevel, - ); + // The work progressed at a different priority, so we can't resume on + // it. But we should stash it away so we can come back to it later at + // the lower priority. + if (progressedWork === workInProgress) { + forkWorkInProgress( + current, + workInProgress, + workInProgress.child, + workInProgress.firstDeletion, + workInProgress.lastDeletion, + workInProgress.memoizedProps, + workInProgress.memoizedState, + workInProgress.updateQueue, + workInProgress.pendingWorkPriority, + ); + } + // Reset the work-in-progress. This makes it a copy of the current fiber. + resetToCurrent(current, workInProgress, renderPriority); } - return finishClassComponent( - current, - workInProgress, - shouldUpdate, - hasContext, - ); + } else { + // If there's no work since the last commit, reset to current. + resetToCurrent(current, workInProgress, renderPriority); } +} - function finishClassComponent( - current: Fiber | null, - workInProgress: Fiber, - shouldUpdate: boolean, - hasContext: boolean, - ) { - // Refs should update even if shouldComponentUpdate returns false - markRef(current, workInProgress); - - if (!shouldUpdate) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } +const BeginWork = function( + config: HostConfig, + hostContext: HostContext, + hydrationContext: HydrationContext, + scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, + getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, +) { + const { + shouldSetTextContent, + useSyncScheduling, + shouldDeprioritizeSubtree, + } = config; - const instance = workInProgress.stateNode; + const {pushHostContext, pushHostContainer} = hostContext; + const classUpdater = ClassUpdater(scheduleUpdate, getPriorityContext); - // Rerender - ReactCurrentOwner.current = workInProgress; - let nextChildren; - if (__DEV__) { - ReactDebugCurrentFiber.phase = 'render'; - nextChildren = instance.render(); - ReactDebugCurrentFiber.phase = null; - } else { - nextChildren = instance.render(); - } - reconcileChildren(current, workInProgress, nextChildren); - // Memoize props and state using the values we just used to render. - // TODO: Restructure so we never read values from the instance. - memoizeState(workInProgress, instance.state); - memoizeProps(workInProgress, instance.props); + const { + enterHydrationState, + resetHydrationState, + tryToClaimNextHydratableInstance, + } = hydrationContext; - // The context might have changed so we need to recalculate it. - if (hasContext) { - invalidateContextProvider(workInProgress); + function checkForUpdatedRef(current: Fiber | null, workInProgress: Fiber) { + const ref = workInProgress.ref; + if (ref !== null && (current === null || current.ref !== ref)) { + // We have a new or updated ref. Schedule a Ref effect so that it + // gets attached during the commit phase. + workInProgress.effectTag |= Ref; } - return workInProgress.child; } - function updateHostRoot(current, workInProgress, priorityLevel) { + function beginHostRoot( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { const root = (workInProgress.stateNode: FiberRoot); if (root.pendingContext) { pushTopLevelContextObject( @@ -343,106 +583,149 @@ module.exports = function( pushHostContainer(workInProgress, root.containerInfo); + const memoizedState = workInProgress.memoizedState; const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - const prevState = workInProgress.memoizedState; - const state = beginUpdateQueue( + const nextState = updateQueue === null + ? memoizedState + : beginUpdateQueue( + current, + workInProgress, + updateQueue, + null, + memoizedState, + null, + renderPriority, + ); + + // Schedule a callback effect if needed. + if ( + workInProgress.updateQueue !== null && + workInProgress.updateQueue.callbackList !== null + ) { + workInProgress.effectTag |= Callback; + } + + if (nextState === memoizedState) { + // No new state. The root doesn't have props. Bailout. + // TODO: What about context? + resetHydrationState(); + return bailout( + current, workInProgress, - updateQueue, - null, - prevState, null, - priorityLevel, + memoizedState, + renderPriority, ); - if (prevState === state) { - // If the state is the same as before, that's a bailout because we had - // no work matching this priority. - resetHydrationState(); - return bailoutOnAlreadyFinishedWork(current, workInProgress); - } - const element = state.element; - if (current === null || current.child === null) { - // If we don't have any current children this might be the first pass. - // We always try to hydrate. If this isn't a hydration pass there won't - // be any children to hydrate which is effectively the same thing as - // not hydrating. - if (enterHydrationState(workInProgress)) { - // This is a bit of a hack. We track the host root as a placement to - // know that we're currently in a mounting state. That way isMounted - // works as expected. We must reset this before committing. - // TODO: Delete this when we delete isMounted and findDOMNode. - workInProgress.effectTag |= Placement; - - // Ensure that children mount into this root without tracking - // side-effects. This ensures that we don't store Placement effects on - // nodes that will be hydrated. - workInProgress.child = mountChildFibersInPlace( - workInProgress, - workInProgress.child, - element, - priorityLevel, - ); - markChildAsProgressed(current, workInProgress, priorityLevel); - return workInProgress.child; - } - } - // Otherwise reset hydration state in case we aborted and resumed another - // root. + } + + // The state was updated. We have a new element. + const nextChildren = nextState.element; + + // If we don't have any current children this might be the first pass. + // We always try to hydrate. If this isn't a hydration pass there won't + // be any children to hydrate which is effectively the same thing as + // not hydrating. + let forceMountInPlace; + if ( + (current === null || current.child === null) && + enterHydrationState(workInProgress) + ) { + // Ensure that children mount into this root without tracking + // side-effects. This ensures that we don't store Placement effects on + // nodes that will be hydrated. + forceMountInPlace = true; + + // This is a bit of a hack. We track the host root as a placement to + // know that we're currently in a mounting state. That way isMounted + // works as expected. We must reset this before committing. + // TODO: Delete this when we delete isMounted and findDOMNode. + workInProgress.effectTag |= Placement; + } else { + // Otherwise, reset the hydration state resetHydrationState(); - reconcileChildren(current, workInProgress, element); - memoizeState(workInProgress, state); - return workInProgress.child; + forceMountInPlace = false; } - resetHydrationState(); - // If there is no update queue, that's a bailout because the root has no props. - return bailoutOnAlreadyFinishedWork(current, workInProgress); + + // Reconcile the children. + const child = (workInProgress.child = reconcileImpl( + current, + workInProgress, + workInProgress.child, + nextChildren, + null, + nextState, + forceMountInPlace, + renderPriority, + )); + return child; } - function updateHostComponent(current, workInProgress) { + function beginHostPortal( + current: Fiber | null, + workInProgress: Fiber, + nextChildren: mixed, + renderPriority: PriorityLevel, + ): Fiber | null { + pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); + + const memoizedChildren = workInProgress.memoizedProps; + if (nextChildren === memoizedChildren && !hasContextChanged()) { + return bailout( + current, + workInProgress, + nextChildren, + null, + renderPriority, + ); + } + + // Reconcile the children. + return reconcile( + current, + workInProgress, + nextChildren, + nextChildren, + null, + renderPriority, + ); + } + + function beginHostComponent( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { + const type = workInProgress.type; + pushHostContext(workInProgress); if (current === null) { tryToClaimNextHydratableInstance(workInProgress); } - let nextProps = workInProgress.pendingProps; - const type = workInProgress.type; - const prevProps = current !== null ? current.memoizedProps : null; const memoizedProps = workInProgress.memoizedProps; - if (hasContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - if (nextProps === null) { - nextProps = memoizedProps; - invariant( - nextProps !== null, - 'We should always have pending or current props. This error is ' + - 'likely caused by a bug in React. Please file an issue.', + + // Check if the ref has changed and schedule an effect. This should happen + // even if we bailout. + checkForUpdatedRef(current, workInProgress); + + // Check the host config to see if the children are offscreen/hidden. + const isHidden = + !useSyncScheduling && shouldDeprioritizeSubtree(type, nextProps); + + if (nextProps === memoizedProps && !hasContextChanged()) { + // Neither props nor context changed. Bailout. + if (isHidden) { + return bailoutHiddenChildren( + current, + workInProgress, + nextProps, + null, + renderPriority, ); } - } else if (nextProps === null || memoizedProps === nextProps) { - if ( - !useSyncScheduling && - shouldDeprioritizeSubtree(type, memoizedProps) && - workInProgress.pendingWorkPriority !== OffscreenPriority - ) { - // This subtree still has work, but it should be deprioritized so we need - // to bail out and not do any work yet. - // TODO: It would be better if this tree got its correct priority set - // during scheduleUpdate instead because otherwise we'll start a higher - // priority reconciliation first before we can get down here. However, - // that is a bit tricky since workInProgress and current can have - // different "hidden" settings. - let child = workInProgress.progressedChild; - while (child !== null) { - // To ensure that this subtree gets its priority reset, the children - // need to be reset. - child.pendingWorkPriority = OffscreenPriority; - child = child.sibling; - } - return null; - } - return bailoutOnAlreadyFinishedWork(current, workInProgress); + return bailout(current, workInProgress, nextProps, null, renderPriority); } let nextChildren = nextProps.children; @@ -454,97 +737,84 @@ module.exports = function( // this in the host environment that also have access to this prop. That // avoids allocating another HostText fiber and traversing it. nextChildren = null; - } else if (prevProps && shouldSetTextContent(type, prevProps)) { + } else if ( + memoizedProps != null && + shouldSetTextContent(type, memoizedProps) + ) { // If we're switching from a direct text child to a normal child, or to // empty, we need to schedule the text content to be reset. workInProgress.effectTag |= ContentReset; } - markRef(current, workInProgress); - - if ( - !useSyncScheduling && - shouldDeprioritizeSubtree(workInProgress.type, nextProps) && - workInProgress.pendingWorkPriority !== OffscreenPriority - ) { - // If this host component is hidden, we can bail out on the children. - // We'll rerender the children later at the lower priority. - - // It is unfortunate that we have to do the reconciliation of these - // children already since that will add them to the tree even though - // they are not actually done yet. If this is a large set it is also - // confusing that this takes time to do right now instead of later. - - if (workInProgress.progressedPriority === OffscreenPriority) { - // If we already made some progress on the offscreen priority before, - // then we should continue from where we left off. - workInProgress.child = workInProgress.progressedChild; - } - - // Reconcile the children and stash them for later work. - reconcileChildrenAtPriority( + if (isHidden) { + return reconcileHiddenChildren( current, workInProgress, nextChildren, - OffscreenPriority, + nextProps, + null, + renderPriority, ); - memoizeProps(workInProgress, nextProps); - workInProgress.child = current !== null ? current.child : null; - - if (current === null) { - // 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 !== null) { - child.effectTag = Placement; - child = child.sibling; - } - } - - // Abort and don't process children yet. - return null; - } else { - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextProps); - return workInProgress.child; } + return reconcile( + current, + workInProgress, + nextChildren, + nextProps, + null, + renderPriority, + ); } - function updateHostText(current, workInProgress) { + function beginHostText( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { if (current === null) { tryToClaimNextHydratableInstance(workInProgress); } - let nextProps = workInProgress.pendingProps; - if (nextProps === null) { - nextProps = workInProgress.memoizedProps; - } - memoizeProps(workInProgress, nextProps); - // Nothing to do here. This is terminal. We'll do the completion step - // immediately after. - return null; + + const memoizedProps = workInProgress.memoizedProps; + if (nextProps === memoizedProps) { + return bailout(current, workInProgress, nextProps, null, renderPriority); + } + // Text nodes don't actually have children, but we call reconcile anyway + // so that the progressed work gets updated. + return reconcile( + current, + workInProgress, + null, + nextProps, + null, + renderPriority, + ); } - function mountIndeterminateComponent(current, workInProgress, priorityLevel) { + function beginIndeterminateComponent( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { invariant( current === null, 'An indeterminate component should never have mounted. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); - var fn = workInProgress.type; - var props = workInProgress.pendingProps; - var unmaskedContext = getUnmaskedContext(workInProgress); - var context = getMaskedContext(workInProgress, unmaskedContext); - var value; + const fn = workInProgress.type; + let unmaskedContext = getUnmaskedContext(workInProgress); + let nextContext = getMaskedContext(workInProgress, unmaskedContext); + // This is either a functional component or a module-style class component. + let value; if (__DEV__) { ReactCurrentOwner.current = workInProgress; - value = fn(props, context); + value = fn(nextProps, nextContext); } else { - value = fn(props, context); + value = fn(nextProps, nextContext); } if ( @@ -552,20 +822,39 @@ module.exports = function( value !== null && typeof value.render === 'function' ) { - // Proceed under the assumption that this is a class instance + // Proceed under the assumption that this is a class instance. workInProgress.tag = ClassComponent; + const instance = (workInProgress.stateNode = value); + const initialState = instance.state; + instance.updater = classUpdater; + instance.context = nextContext; + ReactInstanceMap.set(instance, workInProgress); // Push context providers early to prevent context stack mismatches. - // During mounting we don't know the child context yet as the instance doesn't exist. - // We will invalidate the child context in finishClassComponent() right after rendering. - const hasContext = pushContextProvider(workInProgress); - adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, priorityLevel); - return finishClassComponent(current, workInProgress, true, hasContext); + // During mounting we don't know the child context yet as the instance + // doesn't exist. We will invalidate the child context right + // after rendering. + const hasChildContext = pushContextProvider(workInProgress); + unmaskedContext = getUnmaskedContext(workInProgress); + nextContext = getMaskedContext(workInProgress, unmaskedContext); + + return beginClassComponentImpl( + current, + workInProgress, + instance, + nextProps, + nextContext, + initialState, + hasChildContext, + renderPriority, + ); } else { // Proceed under the assumption that this is a functional component workInProgress.tag = FunctionalComponent; + const nextChildren = value; + if (__DEV__) { + // Mount warnings for functional components const Component = workInProgress.type; if (Component) { @@ -599,268 +888,628 @@ module.exports = function( } } } - reconcileChildren(current, workInProgress, value); - memoizeProps(workInProgress, props); - return workInProgress.child; + // Reconcile the children. + return reconcile( + current, + workInProgress, + nextChildren, + nextProps, + null, + renderPriority, + ); } } - function updateCoroutineComponent(current, workInProgress) { - var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine); - if (hasContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - if (nextCoroutine === null) { - nextCoroutine = current && current.memoizedProps; - invariant( - nextCoroutine !== null, - 'We should always have pending or current props. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - } - } else if ( - nextCoroutine === null || - workInProgress.memoizedProps === nextCoroutine + function beginFunctionalComponent( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { + const fn = workInProgress.type; + + const memoizedProps = workInProgress.memoizedProps; + if ( + (nextProps === memoizedProps && !hasContextChanged()) || + // TODO: Disable this before release, since it is not part of the public + // API. I use this for testing to compare the relative overhead + // of classes. + (typeof fn.shouldComponentUpdate === 'function' && + !fn.shouldComponentUpdate(memoizedProps, nextProps)) ) { - nextCoroutine = workInProgress.memoizedProps; - // TODO: When bailing out, we might need to return the stateNode instead - // of the child. To check it for work. - // return bailoutOnAlreadyFinishedWork(current, workInProgress); + // No changes to props or context. Bailout. + return bailout(current, workInProgress, nextProps, null, renderPriority); } - const nextChildren = nextCoroutine.children; - const priorityLevel = workInProgress.pendingWorkPriority; + const unmaskedContext = getUnmaskedContext(workInProgress); + const nextContext = getMaskedContext(workInProgress, unmaskedContext); - // The following is a fork of reconcileChildrenAtPriority but using - // stateNode to store the child. + // Compute the next children. + let nextChildren; + if (__DEV__) { + // In DEV, track the current owner for better stack traces + ReactCurrentOwner.current = workInProgress; + ReactDebugCurrentFiber.phase = 'render'; + nextChildren = fn(nextProps, nextContext); + ReactDebugCurrentFiber.phase = null; + } else { + nextChildren = fn(nextProps, nextContext); + } - // At this point any memoization is no longer valid since we'll have changed - // the children. - workInProgress.memoizedProps = null; - if (current === null) { - workInProgress.stateNode = mountChildFibersInPlace( - workInProgress, - workInProgress.stateNode, - nextChildren, - priorityLevel, - ); - } else if (current.child === workInProgress.child) { - clearDeletions(workInProgress); + // Reconcile the children. + return reconcile( + current, + workInProgress, + nextChildren, + nextProps, + null, + renderPriority, + ); + } - workInProgress.stateNode = reconcileChildFibers( - workInProgress, - workInProgress.stateNode, - nextChildren, - priorityLevel, - ); + // ----------------- The Life-Cycle of a Composite Component ----------------- + // The begin phase (or render phase) of a composite component is when we call + // the render method to compute the next set of children. Some lifecycle + // methods are also called during this phase. These methods make up the bulk + // of a React app's total execution time. + // + // The begin phase is the part of the React update cycle that is asynchronous + // and time-sliced. Ideally, methods in this phase contain no side-effects + // (other than scheduling updates with setState, which is fine because the + // update queue is managed by React). At the very least, lifecycles in the + // begin phase should be resilient to renders that are interrupted, restarted, + // or aborted. E.g. componentWillMount may fire twice before its children + // are inserted. + // + // Overview of the composite component begin phase algorithm: + // - Do we have new props or context since the last render? + // -> componentWillReceiveProps(nextProps, nextContext). + // - Process the update queue to compute the next state. + // - Do we have new props, context, or state since the last render? + // - If they are unchanged -> bailout. Stop working and don't re-render. + // - If something did change, we may be able to bailout anyway: + // - Is this a forced update (caused by this.forceUpdate())? + // -> Can't bailout. Skip subsequent checks and continue rendering. + // - Is shouldComponentUpdate defined? + // -> shouldComponentUpdate(nextProps, nextState, nextContext) + // - If it returns false -> bailout. + // - Is this a PureComponent? + // -> Shallow compare props and state. + // - If they are the same -> bailout. + // - Proceed with rendering. Are we mounting a new component, or updating + // an existing one? + // - Mount -> componentWillMount() + // - Update -> componentWillUpdate(nextProps, nextState, nextContext) + // - Call render method to compute next children. + // - Reconcile next children against the previous set. + // - Enter begin phase for children. + // + // componentDidMount, componentDidUpdate, and componentWillUnount are called + // during the commit phase, along with other side-effects like refs, + // callbacks, and host mutations (e.g. updating the DOM). + // --------------------------------------------------------------------------- + function beginClassComponent( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { + // Push context providers early to prevent context stack mismatches. During + // mounting we don't know the child context yet as the instance doesn't + // exist. We will invalidate the child context right after rendering. + const hasChildContext = pushContextProvider(workInProgress); + + const ctor = workInProgress.type; + + const unmaskedContext = getUnmaskedContext(workInProgress); + const nextContext = getMaskedContext(workInProgress, unmaskedContext); + + let instance = workInProgress.stateNode; + let previousState; + if (instance === null) { + // This is a fresh component. Construct the public component instance. + instance = workInProgress.stateNode = new ctor(nextProps, nextContext); + const initialState = (previousState = instance.state); + instance.updater = classUpdater; + instance.context = nextContext; + ReactInstanceMap.set(instance, workInProgress); + validateClassInstance(workInProgress, nextProps, initialState); - transferDeletions(workInProgress); + if ( + ReactFeatureFlags.enableAsyncSubtreeAPI && + ctor.unstable_asyncUpdates === true + ) { + // This is a special async wrapper component. Enable async scheduling + // for this component and all of its children. + workInProgress.internalContextTag |= AsyncUpdates; + } } else { - workInProgress.stateNode = reconcileChildFibersInPlace( - workInProgress, - workInProgress.stateNode, - nextChildren, - priorityLevel, - ); - - transferDeletions(workInProgress); + previousState = workInProgress.memoizedState; } - memoizeProps(workInProgress, nextCoroutine); - // This doesn't take arbitrary time so we could synchronously just begin - // eagerly do the work of workInProgress.child as an optimization. - return workInProgress.stateNode; + return beginClassComponentImpl( + current, + workInProgress, + instance, + nextProps, + nextContext, + previousState, + hasChildContext, + renderPriority, + ); } - function updatePortalComponent(current, workInProgress) { - pushHostContainer(workInProgress, workInProgress.stateNode.containerInfo); - const priorityLevel = workInProgress.pendingWorkPriority; - let nextChildren = workInProgress.pendingProps; - if (hasContextChanged()) { - // Normally we can bail out on props equality but if context has changed - // we don't do the bailout and we have to reuse existing props instead. - if (nextChildren === null) { - nextChildren = current && current.memoizedProps; - invariant( - nextChildren != null, - 'We should always have pending or current props. This error is ' + - 'likely caused by a bug in React. Please file an issue.', + // Split this out so that it can be shared between beginClassComponent and + // beginIndeterminateComponent, which have different ways of constructing + // the class instance. By the time this method is called, we already have a + // class instance. + function beginClassComponentImpl( + current: Fiber | null, + workInProgress: Fiber, + instance: any, + nextProps: any, + nextContext: mixed, + // The memoized state, or the initial state for new components + previousState: mixed, + hasChildContext: boolean, + renderPriority: PriorityLevel, + ): Fiber | null { + const ctor = workInProgress.type; + const contextDidChange = hasContextChanged(); + // TODO: Is there a better way to get the memoized context besides reading + // from the instance? + const memoizedContext = instance.context; + const memoizedProps = workInProgress.memoizedProps; + // Don't process the update queue until after componentWillReceiveProps + const memoizedState = previousState; + let nextState = previousState; + + // Is this the initial render? (Note: different from whether this is initial + // mount, since a component may render multiple times before mounting.) + const isInitialRender = memoizedProps === null; + + // Check if this is a new component or an update. + if (!isInitialRender && (nextProps !== memoizedProps || contextDidChange)) { + // This component has been rendered before, and it has received new props + // or context since the last render. Call componentWillReceiveProps, if + // it exists. This should be called even if the component hasn't mounted + // yet (current === null) so that state derived from props stays in sync. + const cWRP = instance.componentWillReceiveProps; + if (typeof cWRP === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillReceiveProps'); + } + callClassInstanceMethod( + instance, + cWRP, + // this.props, this.context, this.state + memoizedProps, + memoizedContext, + memoizedState, + // Arguments + nextProps, + nextContext, ); + if (__DEV__) { + stopPhaseTimer(); + } + // Detect direct assignment to this.state. + // TODO: Ideally, we should never reference read from the public + // instance. It'd be nice to remove support for this eventually. + if (instance.state !== memoizedState) { + if (__DEV__) { + warning( + false, + '%s.componentWillReceiveProps(): Assigning directly to ' + + "this.state is deprecated (except inside a component's " + + 'constructor). Use setState instead.', + getComponentName(workInProgress), + ); + } + classUpdater.enqueueReplaceState(instance, instance.state, null); + } } - } else if ( - nextChildren === null || - workInProgress.memoizedProps === nextChildren - ) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); } - if (current === null) { - // Portals are special because we don't append the children during mount - // but at commit. Therefore we need to track insertions which the normal - // flow doesn't do during mount. This doesn't happen at the root because - // the root always starts with a "current" with a null child. - // TODO: Consider unifying this with how the root works. - workInProgress.child = reconcileChildFibersInPlace( + // Process all the updates in the update queue that satisfy our current + // render priority. This will produce a new state object that we can compare + // to the memoized state. + if (workInProgress.updateQueue !== null) { + nextState = beginUpdateQueue( + current, workInProgress, - workInProgress.child, - nextChildren, - priorityLevel, + workInProgress.updateQueue, + instance, + nextState, + nextProps, + renderPriority, ); - memoizeProps(workInProgress, nextChildren); - markChildAsProgressed(current, workInProgress, priorityLevel); - } else { - reconcileChildren(current, workInProgress, nextChildren); - memoizeProps(workInProgress, nextChildren); } - return workInProgress.child; - } - /* - function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) { - let child = firstChild; - do { - // Ensure that the first and last effect of the parent corresponds - // to the children's first and last effect. - if (!returnFiber.firstEffect) { - returnFiber.firstEffect = child.firstEffect; + // Compare the next inputs (props, context, state) to the memoized inputs + // to determine if we should re-render the children or bailout. + let shouldUpdate; + if (isInitialRender) { + shouldUpdate = true; + } else if ( + workInProgress.updateQueue !== null && + workInProgress.updateQueue.hasForceUpdate + ) { + // This is a forced update. Re-render regardless of shouldComponentUpdate. + shouldUpdate = true; + } else if ( + nextProps === memoizedProps && + nextState === memoizedState && + !contextDidChange + ) { + // None of the inputs have changed. Bailout. + shouldUpdate = false; + } else if (typeof instance.shouldComponentUpdate === 'function') { + // There was a change in props, state, or context. But we may be able to + // bailout anyway if shouldComponentUpdate -> false. + if (__DEV__) { + startPhaseTimer(workInProgress, 'shouldComponentUpdate'); + } + shouldUpdate = callClassInstanceMethod( + instance, + instance.shouldComponentUpdate, + // this.props, this.context, this.state + memoizedProps, + memoizedContext, + memoizedState, + // Arguments + nextProps, + nextState, + nextContext, + ); + if (__DEV__) { + stopPhaseTimer(); + warning( + shouldUpdate !== undefined, + '%s.shouldComponentUpdate(): Returned undefined instead of a ' + + 'boolean value. Make sure to return true or false.', + getComponentName(workInProgress) || 'Unknown', + ); } - if (child.lastEffect) { - if (returnFiber.lastEffect) { - returnFiber.lastEffect.nextEffect = child.firstEffect; + } else if (ctor.prototype && ctor.prototype.isPureReactComponent) { + // This is a PureComponent. Do a shallow comparison of props and state. + shouldUpdate = + !shallowEqual(memoizedProps, nextProps) || + !shallowEqual(memoizedState, nextState); + } else { + // The inputs changed and we can't bail out. Re-render. + shouldUpdate = true; + } + + if (shouldUpdate) { + // If this is not a bailout, call componentWillMount (if this is a mount) + // or componentWillUpdate (if this is an update). + if (current === null) { + // This is a mount. That doesn't mean we haven't rendered this component + // before — a previous mount may have been interrupted. Regardless, call + // componentWillMount, if it exists. + const cWM = instance.componentWillMount; + if (typeof cWM === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillMount'); + } + callClassInstanceMethod( + instance, + cWM, + // this.props, this.context, this.state + nextProps, + nextContext, + nextState, + // No arguments + ); + if (__DEV__) { + stopPhaseTimer(); + } + // Detect direct assignment to this.state. + // TODO: Ideally, we should never reference read from the public + // instance. It'd be nice to remove support for this eventually. + if (instance.state !== nextState) { + if (__DEV__) { + warning( + false, + '%s.componentWillMount(): Assigning directly to this.state ' + + "is deprecated (except inside a component's constructor). " + + 'Use setState instead.', + getComponentName(workInProgress), + ); + } + classUpdater.enqueueReplaceState(instance, instance.state, null); + } + } + } else { + // This is an update. Call componentWillUpdate, if it exists. + const cWU = instance.componentWillUpdate; + if (typeof cWU === 'function') { + if (__DEV__) { + startPhaseTimer(workInProgress, 'componentWillUpdate'); + } + callClassInstanceMethod( + instance, + cWU, + // this.props, this.context, this.state + memoizedProps, + memoizedContext, + memoizedState, + // Arguments + // (The asymmetry between the signatures for componentWillMount and + // componentWillUpdate is confusing. Oh well, can't change it now.) + nextProps, + nextState, + nextContext, + ); + if (__DEV__) { + stopPhaseTimer(); + } + // Unlike cWRP and cWM, we don't support direct assignment to + // this.state inside cWU. We only support it (with a warning) in those + // other methods because it happened to work in Stack, and we don't + // want to break existing product code. } - returnFiber.lastEffect = child.lastEffect; } - } while (child = child.sibling); - } - */ - function bailoutOnAlreadyFinishedWork( - current, - workInProgress: Fiber, - ): Fiber | null { - if (__DEV__) { - cancelWorkTimer(workInProgress); - } - - const priorityLevel = workInProgress.pendingWorkPriority; - // TODO: We should ideally be able to bail out early if the children have no - // more work to do. However, since we don't have a separation of this - // Fiber's priority and its children yet - we don't know without doing lots - // of the same work we do anyway. Once we have that separation we can just - // bail out here if the children has no more work at this priority level. - // if (workInProgress.priorityOfChildren <= priorityLevel) { - // // If there are side-effects in these children that have not yet been - // // committed we need to ensure that they get properly transferred up. - // if (current && current.child !== workInProgress.child) { - // reuseChildrenEffects(workInProgress, child); - // } - // 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; - } + // Process the update queue again in case cWM or cWU contained updates. + if (workInProgress.updateQueue !== null) { + nextState = beginUpdateQueue( + current, + workInProgress, + workInProgress.updateQueue, + instance, + nextState, + nextProps, + renderPriority, + ); + } + } + + // Determine if any effects need to be scheduled. These should all happen + // before bailing out because the effectTag gets reset during reconcilation. + // It should also happen after any lifecycles, in case they contain updates. + + // Check if the ref has changed and schedule an effect. + checkForUpdatedRef(current, workInProgress); + + // Schedule a callback effect if needed. + if ( + workInProgress.updateQueue !== null && + workInProgress.updateQueue.callbackList !== null + ) { + workInProgress.effectTag |= Callback; + } + + // If we have new props or state since the last commit (includes the + // initial mount), we need to schedule an Update effect. This could be + // true even if we're about to bailout (shouldUpdate === false), because + // a bailout only means that the work-in-progress is up-to-date; it may not + // have ever committed. Instead, we need to compare to current to see if + // anything changed. + if (current === null) { + if (typeof instance.componentDidMount === 'function') { + workInProgress.effectTag |= Update; + } + } else if ( + // TODO: We compare the memoizedProps and memoizedState to current so + // that it evaluates to false for a shouldComponentUpdate -> false + // bailout. However, this will fail if we bailout with + // shouldComponentUpdate twice before committing, because the props and + // state are updated after the first bailout. Only observable in async + // mode. Need to find a better heuristic. We can't read from the effectTag + // because it may have been reset during reconciliation. + shouldUpdate || + current.memoizedProps !== workInProgress.memoizedProps || + current.memoizedState !== workInProgress.memoizedState + ) { + if (typeof instance.componentDidUpdate === 'function') { + workInProgress.effectTag |= Update; + } + } - function bailoutOnLowPriority(current, workInProgress) { + // By now, all effects should have been scheduled. It's safe to bailout. + if (!shouldUpdate) { + // This is a bailout. Reuse the work without re-rendering. + return bailout( + current, + workInProgress, + nextProps, + nextState, + renderPriority, + ); + } + + // No bailout. Call the render method to get the next set of children. + ReactCurrentOwner.current = workInProgress; + if (__DEV__) { + ReactDebugCurrentFiber.phase = 'render'; + } + const nextChildren = callClassInstanceMethod( + instance, + instance.render, + // this.props, this.context, this.state + nextProps, + nextContext, + nextState, + // No arguments + ); if (__DEV__) { - cancelWorkTimer(workInProgress); + ReactDebugCurrentFiber.phase = null; } - // TODO: Handle HostComponent tags here as well and call pushHostContext()? - // See PR 8590 discussion for context - switch (workInProgress.tag) { - case ClassComponent: - pushContextProvider(workInProgress); - break; - case HostPortal: - pushHostContainer( - workInProgress, - workInProgress.stateNode.containerInfo, - ); - break; + // If this component provides context to its children, we need to + // recalcuate it before we start working on them. + if (hasChildContext) { + invalidateContextProvider(workInProgress); } - // TODO: What if this is currently in progress? - // How can that happen? How is this not being cloned? - return null; + + // Reconcile the children. + return reconcile( + current, + workInProgress, + nextChildren, + nextProps, + nextState, + renderPriority, + ); } - function memoizeProps(workInProgress: Fiber, nextProps: any) { - workInProgress.memoizedProps = nextProps; - // Reset the pending props - workInProgress.pendingProps = null; + function beginFragment( + current: Fiber | null, + workInProgress: Fiber, + nextProps: any, + renderPriority: PriorityLevel, + ): Fiber | null { + const memoizedProps = workInProgress.memoizedProps; + if (nextProps === memoizedProps && !hasContextChanged()) { + // No changes to props or context. Bailout. + return bailout(current, workInProgress, nextProps, null, renderPriority); + } + + // Compute the next children. + const nextChildren = nextProps; + return reconcile( + current, + workInProgress, + nextChildren, + nextProps, + null, + renderPriority, + ); } - function memoizeState(workInProgress: Fiber, nextState: any) { - workInProgress.memoizedState = nextState; - // Don't reset the updateQueue, in case there are pending updates. Resetting - // is handled by beginUpdateQueue. + function beginCoroutineComponent( + current: Fiber | null, + workInProgress: Fiber, + nextCoroutine: ReactCoroutine, + renderPriority: PriorityLevel, + ): Fiber | null { + // TODO: Bailout if coroutine hasn't changed. When bailing out, we might + // need to return the stateNode instead of the child. To check it for work. + const child = (workInProgress.stateNode = reconcileImpl( + current, + workInProgress, + workInProgress.stateNode, + nextCoroutine.children, + nextCoroutine, + null, + false, + renderPriority, + )); + return child; } function beginWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderPriority: PriorityLevel, ): Fiber | null { - if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel - ) { - return bailoutOnLowPriority(current, workInProgress); - } - if (__DEV__) { + // Keep track of the fiber we're currently working on. ReactDebugCurrentFiber.current = workInProgress; } - // If we don't bail out, we're going be recomputing our children so we need - // to drop our effect list. + resumeOrResetWork(current, workInProgress, renderPriority); + + // Clear the effect list, as it's no longer valid. workInProgress.firstEffect = null; + workInProgress.nextEffect = 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. - workInProgress.child = workInProgress.progressedChild; + let nextProps = workInProgress.pendingProps; + if (nextProps === null) { + // If there are no pending props, re-use the memoized props. + nextProps = workInProgress.memoizedProps; + invariant(nextProps !== null, 'Must have pending or memoized props.'); + } else { + // Reset the pending props, since we're about to process them. + workInProgress.pendingProps = null; } + let next = null; switch (workInProgress.tag) { + case HostRoot: + next = beginHostRoot( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; + case HostPortal: + next = beginHostPortal( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; + case HostComponent: + next = beginHostComponent( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; + case HostText: + next = beginHostText( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; case IndeterminateComponent: - return mountIndeterminateComponent( + next = beginIndeterminateComponent( current, workInProgress, - priorityLevel, + nextProps, + renderPriority, ); + break; case FunctionalComponent: - return updateFunctionalComponent(current, workInProgress); + next = beginFunctionalComponent( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; case ClassComponent: - return updateClassComponent(current, workInProgress, priorityLevel); - case HostRoot: - return updateHostRoot(current, workInProgress, priorityLevel); - case HostComponent: - return updateHostComponent(current, workInProgress); - case HostText: - return updateHostText(current, workInProgress); + next = beginClassComponent( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; + case Fragment: + next = beginFragment( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; case CoroutineHandlerPhase: // This is a restart. Reset the tag to the initial phase. workInProgress.tag = CoroutineComponent; // Intentionally fall through since this is now the same. case CoroutineComponent: - return updateCoroutineComponent(current, workInProgress); + next = beginCoroutineComponent( + current, + workInProgress, + nextProps, + renderPriority, + ); + break; case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. - return null; - case HostPortal: - return updatePortalComponent(current, workInProgress); - case Fragment: - return updateFragment(current, workInProgress); + next = null; + break; default: invariant( false, @@ -868,12 +1517,23 @@ module.exports = function( 'React. Please file an issue.', ); } + + // Mark this as the newest work. This is not the same as progressed work, + // which only includes re-renders/updates. Keeping track of the lastest + // update OR bailout lets us make sure that we don't mistake a "previous + // current" fiber for a fresh work-in-progress. + workInProgress.newestWork = workInProgress; + if (current !== null) { + current.newestWork = workInProgress; + } + + return next; } function beginFailedWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderPriority: PriorityLevel, ) { invariant( workInProgress.tag === ClassComponent || workInProgress.tag === HostRoot, @@ -881,33 +1541,40 @@ module.exports = function( 'Please file an issue.', ); - // Add an error effect so we can handle the error during the commit phase - workInProgress.effectTag |= Err; - - if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel - ) { - return bailoutOnLowPriority(current, workInProgress); + const memoizedProps = workInProgress.memoizedProps; + let nextProps = workInProgress.pendingProps; + if (nextProps === null) { + nextProps = memoizedProps; } - // If we don't bail out, we're going be recomputing our children so we need - // to drop our effect list. + // Clear any pending props or updates. + workInProgress.pendingProps = null; + workInProgress.updateQueue = null; + + // Clear the effect list, as it's no longer valid. workInProgress.firstEffect = null; workInProgress.lastEffect = null; - // Unmount the current children as if the component rendered null + // Add an error effect so we can handle the error during the commit phase + workInProgress.effectTag |= Err; + + // Unmount the children const nextChildren = null; - reconcileChildren(current, workInProgress, nextChildren); + const next = reconcile( + current, + workInProgress, + nextChildren, + memoizedProps, + workInProgress.memoizedState, + renderPriority, + ); - if (workInProgress.tag === ClassComponent) { - const instance = workInProgress.stateNode; - workInProgress.memoizedProps = instance.props; - workInProgress.memoizedState = instance.state; - workInProgress.pendingProps = null; + workInProgress.newestWork = workInProgress; + if (current !== null) { + current.newestWork = workInProgress; } - return workInProgress.child; + return next; } return { @@ -915,3 +1582,4 @@ module.exports = function( beginFailedWork, }; }; +exports.BeginWork = BeginWork; diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 1d695f7cb2e3..9a049e6697ec 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -15,35 +15,19 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; -var {Update} = require('ReactTypeOfSideEffect'); - -var ReactFeatureFlags = require('ReactFeatureFlags'); -var {AsyncUpdates} = require('ReactTypeOfInternalContext'); - -var { - cacheContext, - getMaskedContext, - getUnmaskedContext, - isContextConsumer, -} = require('ReactFiberContext'); var { addUpdate, addReplaceUpdate, addForceUpdate, - beginUpdateQueue, } = require('ReactFiberUpdateQueue'); -var {hasContextChanged} = require('ReactFiberContext'); var {isMounted} = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); -var emptyObject = require('fbjs/lib/emptyObject'); var getComponentName = require('getComponentName'); -var shallowEqual = require('fbjs/lib/shallowEqual'); var invariant = require('fbjs/lib/invariant'); const isArray = Array.isArray; if (__DEV__) { - var {startPhaseTimer, stopPhaseTimer} = require('ReactDebugFiberPerf'); var warning = require('fbjs/lib/warning'); var warnOnInvalidCallback = function(callback: mixed, callerName: string) { warning( @@ -56,16 +40,163 @@ if (__DEV__) { }; } -module.exports = function( +// Call immediately after constructing a class instance. +function validateClassInstance( + workInProgress: Fiber, + initialProps: mixed, + initialState: mixed, +) { + const instance = workInProgress.stateNode; + const ctor = workInProgress.type; + if (__DEV__) { + const name = getComponentName(workInProgress); + const renderPresent = instance.render; + warning( + renderPresent, + '%s(...): No `render` method found on the returned component ' + + 'instance: you may have forgotten to define `render`.', + name, + ); + const noGetInitialStateOnES6 = + !instance.getInitialState || + instance.getInitialState.isReactClassApproved || + initialState; + warning( + noGetInitialStateOnES6, + 'getInitialState was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Did you mean to define a state property instead?', + name, + ); + const noGetDefaultPropsOnES6 = + !instance.getDefaultProps || + instance.getDefaultProps.isReactClassApproved; + warning( + noGetDefaultPropsOnES6, + 'getDefaultProps was defined on %s, a plain JavaScript class. ' + + 'This is only supported for classes created using React.createClass. ' + + 'Use a static property to define defaultProps instead.', + name, + ); + const noInstancePropTypes = !instance.propTypes; + warning( + noInstancePropTypes, + 'propTypes was defined as an instance property on %s. Use a static ' + + 'property to define propTypes instead.', + name, + ); + const noInstanceContextTypes = !instance.contextTypes; + warning( + noInstanceContextTypes, + 'contextTypes was defined as an instance property on %s. Use a static ' + + 'property to define contextTypes instead.', + name, + ); + const noComponentShouldUpdate = + typeof instance.componentShouldUpdate !== 'function'; + warning( + noComponentShouldUpdate, + '%s has a method called ' + + 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + + 'The name is phrased as a question because the function is ' + + 'expected to return a value.', + name, + ); + if ( + ctor.prototype && + ctor.prototype.isPureReactComponent && + typeof instance.shouldComponentUpdate !== 'undefined' + ) { + warning( + false, + '%s has a method called shouldComponentUpdate(). ' + + 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + + 'Please extend React.Component if shouldComponentUpdate is used.', + getComponentName(workInProgress) || 'A pure component', + ); + } + const noComponentDidUnmount = + typeof instance.componentDidUnmount !== 'function'; + warning( + noComponentDidUnmount, + '%s has a method called ' + + 'componentDidUnmount(). But there is no such lifecycle method. ' + + 'Did you mean componentWillUnmount()?', + name, + ); + const noComponentWillRecieveProps = + typeof instance.componentWillRecieveProps !== 'function'; + warning( + noComponentWillRecieveProps, + '%s has a method called ' + + 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', + name, + ); + const hasMutatedProps = instance.props !== initialProps; + warning( + instance.props === undefined || !hasMutatedProps, + '%s(...): When calling super() in `%s`, make sure to pass ' + + "up the same props that your component's constructor was passed.", + name, + name, + ); + const noInstanceDefaultProps = !instance.defaultProps; + warning( + noInstanceDefaultProps, + 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' + + ' Instead, define defaultProps as a static property on %s.', + name, + name, + ); + } + if ( + initialState !== undefined && + (typeof initialState !== 'object' || isArray(initialState)) + ) { + invariant( + false, + '%s.state: must be set to an object or null', + getComponentName(workInProgress), + ); + } + if (typeof instance.getChildContext === 'function') { + invariant( + typeof workInProgress.type.childContextTypes === 'object', + '%s.getChildContext(): childContextTypes must be defined in order to ' + + 'use getChildContext().', + getComponentName(workInProgress), + ); + } +} +exports.validateClassInstance = validateClassInstance; + +type Callback = () => mixed; + +function callClassInstanceMethod( + instance: any, + lifecycle: (a: A, b: B, c: C) => D, + instanceProps: mixed, + instanceContext: mixed, + instanceState: mixed, + a: A, + b: B, + c: C, +): D | void { + instance.props = instanceProps; + instance.context = instanceContext; + instance.state = instanceState; + const args = Array.prototype.slice.call(arguments, 5); + return lifecycle.apply(instance, args); +} +exports.callClassInstanceMethod = callClassInstanceMethod; + +function ClassUpdater( scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, - memoizeProps: (workInProgress: Fiber, props: any) => void, - memoizeState: (workInProgress: Fiber, state: any) => void, ) { - // Class component state updater - const updater = { + return { isMounted, - enqueueSetState(instance, partialState, callback) { + enqueueSetState(instance: any, partialState: mixed, callback: ?Callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); callback = callback === undefined ? null : callback; @@ -75,7 +206,7 @@ module.exports = function( addUpdate(fiber, partialState, callback, priorityLevel); scheduleUpdate(fiber, priorityLevel); }, - enqueueReplaceState(instance, state, callback) { + enqueueReplaceState(instance: any, state: mixed, callback: ?Callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); callback = callback === undefined ? null : callback; @@ -85,7 +216,7 @@ module.exports = function( addReplaceUpdate(fiber, state, callback, priorityLevel); scheduleUpdate(fiber, priorityLevel); }, - enqueueForceUpdate(instance, callback) { + enqueueForceUpdate(instance: any, callback: ?Callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); callback = callback === undefined ? null : callback; @@ -96,559 +227,5 @@ module.exports = function( scheduleUpdate(fiber, priorityLevel); }, }; - - function checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ) { - if ( - oldProps === null || - (workInProgress.updateQueue !== null && - workInProgress.updateQueue.hasForceUpdate) - ) { - // If the workInProgress already has an Update effect, return true - return true; - } - - const instance = workInProgress.stateNode; - const type = workInProgress.type; - if (typeof instance.shouldComponentUpdate === 'function') { - if (__DEV__) { - startPhaseTimer(workInProgress, 'shouldComponentUpdate'); - } - const shouldUpdate = instance.shouldComponentUpdate( - newProps, - newState, - newContext, - ); - if (__DEV__) { - stopPhaseTimer(); - } - - if (__DEV__) { - warning( - shouldUpdate !== undefined, - '%s.shouldComponentUpdate(): Returned undefined instead of a ' + - 'boolean value. Make sure to return true or false.', - getComponentName(workInProgress) || 'Unknown', - ); - } - - return shouldUpdate; - } - - if (type.prototype && type.prototype.isPureReactComponent) { - return ( - !shallowEqual(oldProps, newProps) || !shallowEqual(oldState, newState) - ); - } - - return true; - } - - function checkClassInstance(workInProgress: Fiber) { - const instance = workInProgress.stateNode; - const type = workInProgress.type; - if (__DEV__) { - const name = getComponentName(workInProgress); - const renderPresent = instance.render; - warning( - renderPresent, - '%s(...): No `render` method found on the returned component ' + - 'instance: you may have forgotten to define `render`.', - name, - ); - const noGetInitialStateOnES6 = - !instance.getInitialState || - instance.getInitialState.isReactClassApproved || - instance.state; - warning( - noGetInitialStateOnES6, - 'getInitialState was defined on %s, a plain JavaScript class. ' + - 'This is only supported for classes created using React.createClass. ' + - 'Did you mean to define a state property instead?', - name, - ); - const noGetDefaultPropsOnES6 = - !instance.getDefaultProps || - instance.getDefaultProps.isReactClassApproved; - warning( - noGetDefaultPropsOnES6, - 'getDefaultProps was defined on %s, a plain JavaScript class. ' + - 'This is only supported for classes created using React.createClass. ' + - 'Use a static property to define defaultProps instead.', - name, - ); - const noInstancePropTypes = !instance.propTypes; - warning( - noInstancePropTypes, - 'propTypes was defined as an instance property on %s. Use a static ' + - 'property to define propTypes instead.', - name, - ); - const noInstanceContextTypes = !instance.contextTypes; - warning( - noInstanceContextTypes, - 'contextTypes was defined as an instance property on %s. Use a static ' + - 'property to define contextTypes instead.', - name, - ); - const noComponentShouldUpdate = - typeof instance.componentShouldUpdate !== 'function'; - warning( - noComponentShouldUpdate, - '%s has a method called ' + - 'componentShouldUpdate(). Did you mean shouldComponentUpdate()? ' + - 'The name is phrased as a question because the function is ' + - 'expected to return a value.', - name, - ); - if ( - type.prototype && - type.prototype.isPureReactComponent && - typeof instance.shouldComponentUpdate !== 'undefined' - ) { - warning( - false, - '%s has a method called shouldComponentUpdate(). ' + - 'shouldComponentUpdate should not be used when extending React.PureComponent. ' + - 'Please extend React.Component if shouldComponentUpdate is used.', - getComponentName(workInProgress) || 'A pure component', - ); - } - const noComponentDidUnmount = - typeof instance.componentDidUnmount !== 'function'; - warning( - noComponentDidUnmount, - '%s has a method called ' + - 'componentDidUnmount(). But there is no such lifecycle method. ' + - 'Did you mean componentWillUnmount()?', - name, - ); - const noComponentWillRecieveProps = - typeof instance.componentWillRecieveProps !== 'function'; - warning( - noComponentWillRecieveProps, - '%s has a method called ' + - 'componentWillRecieveProps(). Did you mean componentWillReceiveProps()?', - name, - ); - const hasMutatedProps = instance.props !== workInProgress.pendingProps; - warning( - instance.props === undefined || !hasMutatedProps, - '%s(...): When calling super() in `%s`, make sure to pass ' + - "up the same props that your component's constructor was passed.", - name, - name, - ); - const noInstanceDefaultProps = !instance.defaultProps; - warning( - noInstanceDefaultProps, - 'Setting defaultProps as an instance property on %s is not supported and will be ignored.' + - ' Instead, define defaultProps as a static property on %s.', - name, - name, - ); - } - - const state = instance.state; - if (state && (typeof state !== 'object' || isArray(state))) { - invariant( - false, - '%s.state: must be set to an object or null', - getComponentName(workInProgress), - ); - } - if (typeof instance.getChildContext === 'function') { - invariant( - typeof workInProgress.type.childContextTypes === 'object', - '%s.getChildContext(): childContextTypes must be defined in order to ' + - 'use getChildContext().', - getComponentName(workInProgress), - ); - } - } - - function resetInputPointers(workInProgress: Fiber, instance: any) { - instance.props = workInProgress.memoizedProps; - instance.state = workInProgress.memoizedState; - } - - function adoptClassInstance(workInProgress: Fiber, instance: any): void { - instance.updater = updater; - workInProgress.stateNode = instance; - // The instance needs access to the fiber so that it can schedule updates - ReactInstanceMap.set(instance, workInProgress); - } - - function constructClassInstance(workInProgress: Fiber, props: any): any { - const ctor = workInProgress.type; - const unmaskedContext = getUnmaskedContext(workInProgress); - const needsContext = isContextConsumer(workInProgress); - const context = needsContext - ? getMaskedContext(workInProgress, unmaskedContext) - : emptyObject; - const instance = new ctor(props, context); - adoptClassInstance(workInProgress, instance); - - // Cache unmasked context so we can avoid recreating masked context unless necessary. - // ReactFiberContext usually updates this cache but can't for newly-created instances. - if (needsContext) { - cacheContext(workInProgress, unmaskedContext, context); - } - - return instance; - } - - function callComponentWillMount(workInProgress, instance) { - if (__DEV__) { - startPhaseTimer(workInProgress, 'componentWillMount'); - } - const oldState = instance.state; - instance.componentWillMount(); - if (__DEV__) { - stopPhaseTimer(); - } - - if (oldState !== instance.state) { - if (__DEV__) { - warning( - false, - '%s.componentWillMount(): Assigning directly to this.state is ' + - "deprecated (except inside a component's " + - 'constructor). Use setState instead.', - getComponentName(workInProgress), - ); - } - updater.enqueueReplaceState(instance, instance.state, null); - } - } - - function callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ) { - if (__DEV__) { - startPhaseTimer(workInProgress, 'componentWillReceiveProps'); - } - const oldState = instance.state; - instance.componentWillReceiveProps(newProps, newContext); - if (__DEV__) { - stopPhaseTimer(); - } - - if (instance.state !== oldState) { - if (__DEV__) { - warning( - false, - '%s.componentWillReceiveProps(): Assigning directly to ' + - "this.state is deprecated (except inside a component's " + - 'constructor). Use setState instead.', - getComponentName(workInProgress), - ); - } - updater.enqueueReplaceState(instance, instance.state, null); - } - } - - // Invokes the mount life-cycles on a previously never rendered instance. - function mountClassInstance( - workInProgress: Fiber, - priorityLevel: PriorityLevel, - ): void { - if (__DEV__) { - checkClassInstance(workInProgress); - } - - const instance = workInProgress.stateNode; - const state = instance.state || null; - - let props = workInProgress.pendingProps; - invariant( - props, - 'There must be pending props for an initial mount. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - - const unmaskedContext = getUnmaskedContext(workInProgress); - - instance.props = props; - instance.state = state; - instance.refs = emptyObject; - instance.context = getMaskedContext(workInProgress, unmaskedContext); - - if ( - ReactFeatureFlags.enableAsyncSubtreeAPI && - workInProgress.type != null && - workInProgress.type.unstable_asyncUpdates === true - ) { - workInProgress.internalContextTag |= AsyncUpdates; - } - - if (typeof instance.componentWillMount === 'function') { - callComponentWillMount(workInProgress, instance); - // If we had additional state updates during this life-cycle, let's - // process them now. - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - instance.state = beginUpdateQueue( - workInProgress, - updateQueue, - instance, - state, - props, - priorityLevel, - ); - } - } - if (typeof instance.componentDidMount === 'function') { - workInProgress.effectTag |= Update; - } - } - - // Called on a preexisting class instance. Returns false if a resumed render - // could be reused. - function resumeMountClassInstance( - workInProgress: Fiber, - priorityLevel: PriorityLevel, - ): boolean { - const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); - - let newState = workInProgress.memoizedState; - let newProps = workInProgress.pendingProps; - if (!newProps) { - // If there isn't any new props, then we'll reuse the memoized props. - // This could be from already completed work. - newProps = workInProgress.memoizedProps; - invariant( - newProps != null, - 'There should always be pending or memoized props. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - } - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); - - const oldContext = instance.context; - const oldProps = workInProgress.memoizedProps; - - if ( - typeof instance.componentWillReceiveProps === 'function' && - (oldProps !== newProps || oldContext !== newContext) - ) { - callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ); - } - - // Process the update queue before calling shouldComponentUpdate - const updateQueue = workInProgress.updateQueue; - if (updateQueue !== null) { - newState = beginUpdateQueue( - workInProgress, - updateQueue, - instance, - newState, - newProps, - priorityLevel, - ); - } - - // TODO: Should we deal with a setState that happened after the last - // componentWillMount and before this componentWillMount? Probably - // unsupported anyway. - - if ( - !checkShouldComponentUpdate( - workInProgress, - workInProgress.memoizedProps, - newProps, - workInProgress.memoizedState, - newState, - newContext, - ) - ) { - // Update the existing instance's state, props, and context pointers even - // though we're bailing out. - instance.props = newProps; - instance.state = newState; - instance.context = newContext; - return false; - } - - // Update the input pointers now so that they are correct when we call - // componentWillMount - instance.props = newProps; - instance.state = newState; - instance.context = newContext; - - if (typeof instance.componentWillMount === 'function') { - callComponentWillMount(workInProgress, instance); - // componentWillMount may have called setState. Process the update queue. - const newUpdateQueue = workInProgress.updateQueue; - if (newUpdateQueue !== null) { - newState = beginUpdateQueue( - workInProgress, - newUpdateQueue, - instance, - newState, - newProps, - priorityLevel, - ); - } - } - - if (typeof instance.componentDidMount === 'function') { - workInProgress.effectTag |= Update; - } - - instance.state = newState; - - return true; - } - - // Invokes the update life-cycles and returns false if it shouldn't rerender. - function updateClassInstance( - current: Fiber, - workInProgress: Fiber, - priorityLevel: PriorityLevel, - ): boolean { - const instance = workInProgress.stateNode; - resetInputPointers(workInProgress, instance); - - const oldProps = workInProgress.memoizedProps; - let newProps = workInProgress.pendingProps; - if (!newProps) { - // If there aren't any new props, then we'll reuse the memoized props. - // This could be from already completed work. - newProps = oldProps; - invariant( - newProps != null, - 'There should always be pending or memoized props. This error is ' + - 'likely caused by a bug in React. Please file an issue.', - ); - } - const oldContext = instance.context; - const newUnmaskedContext = getUnmaskedContext(workInProgress); - const newContext = getMaskedContext(workInProgress, newUnmaskedContext); - - // Note: During these life-cycles, instance.props/instance.state are what - // ever the previously attempted to render - not the "current". However, - // during componentDidUpdate we pass the "current" props. - - if ( - typeof instance.componentWillReceiveProps === 'function' && - (oldProps !== newProps || oldContext !== newContext) - ) { - callComponentWillReceiveProps( - workInProgress, - instance, - newProps, - newContext, - ); - } - - // Compute the next state using the memoized state and the update queue. - const updateQueue = workInProgress.updateQueue; - const oldState = workInProgress.memoizedState; - // TODO: Previous state can be null. - let newState; - if (updateQueue !== null) { - newState = beginUpdateQueue( - workInProgress, - updateQueue, - instance, - oldState, - newProps, - priorityLevel, - ); - } else { - newState = oldState; - } - - if ( - oldProps === newProps && - oldState === newState && - !hasContextChanged() && - !(updateQueue !== null && updateQueue.hasForceUpdate) - ) { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (typeof instance.componentDidUpdate === 'function') { - if ( - oldProps !== current.memoizedProps || - oldState !== current.memoizedState - ) { - workInProgress.effectTag |= Update; - } - } - return false; - } - - const shouldUpdate = checkShouldComponentUpdate( - workInProgress, - oldProps, - newProps, - oldState, - newState, - newContext, - ); - - if (shouldUpdate) { - if (typeof instance.componentWillUpdate === 'function') { - if (__DEV__) { - startPhaseTimer(workInProgress, 'componentWillUpdate'); - } - instance.componentWillUpdate(newProps, newState, newContext); - if (__DEV__) { - stopPhaseTimer(); - } - } - if (typeof instance.componentDidUpdate === 'function') { - workInProgress.effectTag |= Update; - } - } else { - // If an update was already in progress, we should schedule an Update - // effect even though we're bailing out, so that cWU/cDU are called. - if (typeof instance.componentDidUpdate === 'function') { - if ( - oldProps !== current.memoizedProps || - oldState !== current.memoizedState - ) { - workInProgress.effectTag |= Update; - } - } - - // If shouldComponentUpdate returned false, we should still update the - // memoized props/state to indicate that this work can be reused. - memoizeProps(workInProgress, newProps); - memoizeState(workInProgress, newState); - } - - // Update the existing instance's state, props, and context pointers even - // if shouldComponentUpdate returns false. - instance.props = newProps; - instance.state = newState; - instance.context = newContext; - - return shouldUpdate; - } - - return { - adoptClassInstance, - constructClassInstance, - mountClassInstance, - resumeMountClassInstance, - updateClassInstance, - }; -}; +} +exports.ClassUpdater = ClassUpdater; diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 7fe0efad7233..8d5f79c19a59 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -24,6 +24,7 @@ var { HostPortal, CoroutineComponent, } = ReactTypeOfWork; +var {callClassInstanceMethod} = require('ReactFiberClassComponent'); var {commitCallbacks} = require('ReactFiberUpdateQueue'); var {onCommitUnmount} = require('ReactFiberDevToolsHook'); var {invokeGuardedCallback} = require('ReactErrorUtils'); @@ -59,7 +60,14 @@ module.exports = function( if (__DEV__) { var callComponentWillUnmountWithTimerInDev = function(current, instance) { startPhaseTimer(current, 'componentWillUnmount'); - instance.componentWillUnmount(); + callClassInstanceMethod( + instance, + instance.componentWillUnmount, + current.memoizedProps, + // TODO: Is there a better way to get the memoized context? + instance.context, + current.memoizedState, + ); stopPhaseTimer(); }; } @@ -79,7 +87,14 @@ module.exports = function( } } else { try { - instance.componentWillUnmount(); + callClassInstanceMethod( + instance, + instance.componentWillUnmount, + current.memoizedProps, + // TODO: Is there a better way to get the memoized context? + instance.context, + current.memoizedState, + ); } catch (unmountError) { captureError(current, unmountError); } @@ -450,17 +465,32 @@ module.exports = function( if (__DEV__) { startPhaseTimer(finishedWork, 'componentDidMount'); } - instance.componentDidMount(); + callClassInstanceMethod( + instance, + instance.componentDidMount, + instance.props, // finishedWork.memoizedProps, + // TODO: Is there a better way to get the memoized context? + instance.context, + instance.state, // finishedWork.memoizedState, + ); if (__DEV__) { stopPhaseTimer(); } } else { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; if (__DEV__) { startPhaseTimer(finishedWork, 'componentDidUpdate'); } - instance.componentDidUpdate(prevProps, prevState); + callClassInstanceMethod( + instance, + instance.componentDidUpdate, + finishedWork.memoizedProps, + // TODO: Is there a better way to get the memoized context? + instance.context, + finishedWork.memoizedState, + // Arguments + current.memoizedProps, + current.memoizedState, + ); if (__DEV__) { stopPhaseTimer(); } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 9e95547b48f0..8dca8760e649 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -18,11 +18,15 @@ import type {HostContext} from 'ReactFiberHostContext'; import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; +import type {PriorityLevel} from 'ReactPriorityLevel'; -var {reconcileChildFibers} = require('ReactChildFiber'); +var { + transferEffectsToParent, + getPriorityFromChildren, + largerPriority, +} = require('ReactFiber'); +var {reconcile} = require('ReactFiberBeginWork'); var {popContextProvider} = require('ReactFiberContext'); -var ReactTypeOfWork = require('ReactTypeOfWork'); -var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); var { IndeterminateComponent, FunctionalComponent, @@ -35,8 +39,9 @@ var { CoroutineHandlerPhase, YieldComponent, Fragment, -} = ReactTypeOfWork; -var {Placement, Ref, Update} = ReactTypeOfSideEffect; +} = require('ReactTypeOfWork'); +var {Placement, Update} = require('ReactTypeOfSideEffect'); +var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); if (__DEV__) { var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); @@ -44,7 +49,7 @@ if (__DEV__) { var invariant = require('fbjs/lib/invariant'); -module.exports = function( +exports.CompleteWork = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, @@ -70,28 +75,6 @@ module.exports = function( popHydrationState, } = hydrationContext; - function markChildAsProgressed(current, workInProgress, priorityLevel) { - // We now have clones. Let's store them as the currently progressed work. - workInProgress.progressedChild = workInProgress.child; - workInProgress.progressedPriority = priorityLevel; - if (current !== null) { - // We also store it on the current. When the alternate swaps in we can - // continue from this point. - current.progressedChild = workInProgress.progressedChild; - current.progressedPriority = workInProgress.progressedPriority; - } - } - - function markUpdate(workInProgress: Fiber) { - // Tag the fiber with an update effect. This turns a Placement into - // an UpdateAndPlacement. - workInProgress.effectTag |= Update; - } - - function markRef(workInProgress: Fiber) { - workInProgress.effectTag |= Ref; - } - function appendAllYields(yields: Array, workInProgress: Fiber) { let node = workInProgress.stateNode; if (node) { @@ -125,8 +108,9 @@ module.exports = function( function moveCoroutineToHandlerPhase( current: Fiber | null, workInProgress: Fiber, - ) { - var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); + renderPriority: PriorityLevel, + ): Fiber | null { + const coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); invariant( coroutine, 'Should be resolved by now. This error is likely caused by a bug in ' + @@ -144,23 +128,20 @@ module.exports = function( // Build up the yields. // TODO: Compare this to a generator or opaque helpers like Children. - var yields: Array = []; + const yields: Array = []; appendAllYields(yields, workInProgress); - var fn = coroutine.handler; - var props = coroutine.props; - var nextChildren = fn(props, yields); + const fn = coroutine.handler; + const props = coroutine.props; + const nextChildren = fn(props, yields); - var currentFirstChild = current !== null ? current.child : null; - // Inherit the priority of the returnFiber. - const priority = workInProgress.pendingWorkPriority; - workInProgress.child = reconcileChildFibers( + return reconcile( + current, workInProgress, - currentFirstChild, nextChildren, - priority, + coroutine, + null, + renderPriority, ); - markChildAsProgressed(current, workInProgress, priority); - return workInProgress.child; } function appendAllChildren(parent: I, workInProgress: Fiber) { @@ -194,18 +175,21 @@ module.exports = function( function completeWork( current: Fiber | null, workInProgress: Fiber, + renderPriority: PriorityLevel, ): Fiber | null { if (__DEV__) { ReactDebugCurrentFiber.current = workInProgress; } + let next = null; + switch (workInProgress.tag) { case FunctionalComponent: - return null; + break; case ClassComponent: { // We are leaving this subtree, so pop context if any. popContextProvider(workInProgress); - return null; + break; } case HostRoot: { // TODO: Pop the host container after #8607 lands. @@ -214,7 +198,6 @@ module.exports = function( fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; } - if (current === null || current.child === null) { // If we hydrated, pop so that we can delete any remaining children // that weren't hydrated. @@ -223,7 +206,7 @@ module.exports = function( // TODO: Delete this when we delete isMounted and findDOMNode. workInProgress.effectTag &= ~Placement; } - return null; + break; } case HostComponent: { popHostContext(workInProgress); @@ -254,20 +237,17 @@ module.exports = function( // If the update payload indicates that there is a change or if there // is a new ref we mark this as an update. if (updatePayload) { - markUpdate(workInProgress); - } - if (current.ref !== workInProgress.ref) { - markRef(workInProgress); + workInProgress.effectTag |= Update; } } else { - if (!newProps) { + if (newProps === null) { invariant( workInProgress.stateNode !== null, 'We must have new props for new mounts. This error is likely ' + 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. - return null; + break; } const currentHostContext = getHostContext(); @@ -304,17 +284,13 @@ module.exports = function( rootContainerInstance, ) ) { - markUpdate(workInProgress); + workInProgress.effectTag |= Update; } } workInProgress.stateNode = instance; - if (workInProgress.ref !== null) { - // If there is a ref on a host node we need to schedule a callback - markRef(workInProgress); - } } - return null; + break; } case HostText: { let newText = workInProgress.memoizedProps; @@ -323,7 +299,7 @@ module.exports = function( // If we have an alternate, that means this is an update and we need // to schedule a side-effect to do the updates. if (oldText !== newText) { - markUpdate(workInProgress); + workInProgress.effectTag |= Update; } } else { if (typeof newText !== 'string') { @@ -333,7 +309,7 @@ module.exports = function( 'caused by a bug in React. Please file an issue.', ); // This can happen when we abort work. - return null; + break; } const rootContainerInstance = getRootHostContainer(); const currentHostContext = getHostContext(); @@ -351,24 +327,29 @@ module.exports = function( } workInProgress.stateNode = textInstance; } - return null; + break; } case CoroutineComponent: - return moveCoroutineToHandlerPhase(current, workInProgress); + next = moveCoroutineToHandlerPhase( + current, + workInProgress, + renderPriority, + ); + break; case CoroutineHandlerPhase: // Reset the tag to now be a first phase coroutine. workInProgress.tag = CoroutineComponent; - return null; + break; case YieldComponent: // Does nothing. - return null; + break; case Fragment: - return null; + break; case HostPortal: // TODO: Only mark this as an update if we have any pending callbacks. - markUpdate(workInProgress); + workInProgress.effectTag |= Update; popHostContainer(workInProgress); - return null; + break; // Error cases case IndeterminateComponent: invariant( @@ -385,6 +366,36 @@ module.exports = function( 'React. Please file an issue.', ); } + + // Work in this tree was just completed. There may be lower priority + // remaining. Reset the work priority by bubbling it up from the children. + // We do this regardless of whether the child is current or + // a work-in-progress, because the current children may have pending work + // that's not in the work-in-progress children. + let remainingWorkPriority = NoWork; + + const progressedWork = workInProgress.progressedWork; + if (progressedWork !== current && progressedWork !== workInProgress) { + remainingWorkPriority = workInProgress.progressedPriority; + } + + // Bubble up priority from the children, unless the children are offscreen, + // in which case work should be deprioritized. + // TODO: How will this work with expiration times? + if (remainingWorkPriority !== OffscreenPriority) { + remainingWorkPriority = largerPriority( + remainingWorkPriority, + getPriorityFromChildren(workInProgress), + ); + } + workInProgress.pendingWorkPriority = remainingWorkPriority; + + // Transfer effects list to the parent + if (workInProgress.return !== null) { + transferEffectsToParent(workInProgress.return, workInProgress); + } + + return next; } return { diff --git a/src/renderers/shared/fiber/ReactFiberHydrationContext.js b/src/renderers/shared/fiber/ReactFiberHydrationContext.js index e3043258dc85..8b0048e3c903 100644 --- a/src/renderers/shared/fiber/ReactFiberHydrationContext.js +++ b/src/renderers/shared/fiber/ReactFiberHydrationContext.js @@ -90,21 +90,15 @@ module.exports = function( childToDelete.stateNode = instance; childToDelete.return = returnFiber; // Deletions are added in reversed order so we add it to the front. - const last = returnFiber.progressedLastDeletion; + const last = returnFiber.lastDeletion; if (last !== null) { last.nextEffect = childToDelete; - returnFiber.progressedLastDeletion = childToDelete; + returnFiber.lastDeletion = childToDelete; } else { - returnFiber.progressedFirstDeletion = returnFiber.progressedLastDeletion = childToDelete; + returnFiber.firstDeletion = returnFiber.lastDeletion = childToDelete; } + childToDelete.nextEffect = null; childToDelete.effectTag = Deletion; - - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = childToDelete; - returnFiber.lastEffect = childToDelete; - } else { - returnFiber.firstEffect = returnFiber.lastEffect = childToDelete; - } } function tryToClaimNextHydratableInstance(fiber: Fiber) { diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 409b70655445..e2f3c19aadc6 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -13,8 +13,10 @@ 'use strict'; import type {Fiber} from 'ReactFiber'; +import type {PriorityLevel} from 'ReactPriorityLevel'; const {createHostRootFiber} = require('ReactFiber'); +const {NoWork} = require('ReactPriorityLevel'); export type FiberRoot = { // Any additional information from the host associated with this root. @@ -25,6 +27,10 @@ export type FiberRoot = { isScheduled: boolean, // The work schedule is a linked list. nextScheduledRoot: FiberRoot | null, + pendingWorkPriority: PriorityLevel, + // The effect list, used during the commit phase. + firstEffect: Fiber | null, + lastEffect: Fiber | null, // Top context object, used by renderSubtreeIntoContainer context: Object | null, pendingContext: Object | null, @@ -39,6 +45,9 @@ exports.createFiberRoot = function(containerInfo: any): FiberRoot { containerInfo: containerInfo, isScheduled: false, nextScheduledRoot: null, + pendingWorkPriority: NoWork, + firstEffect: null, + lastEffect: null, context: null, pendingContext: null, }; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index e010390bc8dd..29ed2afbe87b 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -40,15 +40,19 @@ var { var {logCapturedError} = require('ReactFiberErrorLogger'); var {invokeGuardedCallback} = require('ReactErrorUtils'); -var ReactFiberBeginWork = require('ReactFiberBeginWork'); -var ReactFiberCompleteWork = require('ReactFiberCompleteWork'); +var {BeginWork} = require('ReactFiberBeginWork'); +var {CompleteWork} = require('ReactFiberCompleteWork'); var ReactFiberCommitWork = require('ReactFiberCommitWork'); var ReactFiberHostContext = require('ReactFiberHostContext'); var ReactFiberHydrationContext = require('ReactFiberHydrationContext'); var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); -var {cloneFiber} = require('ReactFiber'); +var { + createWorkInProgress, + largerPriority, + transferEffectsToParent, +} = require('ReactFiber'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -64,7 +68,6 @@ var { var {AsyncUpdates} = require('ReactTypeOfInternalContext'); var { - NoEffect, Placement, Update, PlacementAndUpdate, @@ -82,11 +85,12 @@ var { ClassComponent, } = require('ReactTypeOfWork'); -var {getPendingPriority} = require('ReactFiberUpdateQueue'); +var {getUpdatePriority} = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); var invariant = require('fbjs/lib/invariant'); +var emptyObject = require('fbjs/lib/emptyObject'); if (__DEV__) { var warning = require('fbjs/lib/warning'); @@ -152,18 +156,14 @@ module.exports = function( C > = ReactFiberHydrationContext(config); const {popHostContainer, popHostContext, resetHostContainer} = hostContext; - const {beginWork, beginFailedWork} = ReactFiberBeginWork( + const {beginWork, beginFailedWork} = BeginWork( config, hostContext, hydrationContext, scheduleUpdate, getPriorityContext, ); - const {completeWork} = ReactFiberCompleteWork( - config, - hostContext, - hydrationContext, - ); + const {completeWork} = CompleteWork(config, hostContext, hydrationContext); const { commitPlacement, commitDeletion, @@ -261,7 +261,7 @@ module.exports = function( // Clear out roots with no more work on them, or if they have uncaught errors while ( nextScheduledRoot !== null && - nextScheduledRoot.current.pendingWorkPriority === NoWork + nextScheduledRoot.pendingWorkPriority === NoWork ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -280,20 +280,18 @@ module.exports = function( // If there's no work on it, it will get unscheduled too. nextScheduledRoot = next; } - let root = nextScheduledRoot; let highestPriorityRoot = null; let highestPriorityLevel = NoWork; while (root !== null) { + const rootPriority = root.pendingWorkPriority; if ( - root.current.pendingWorkPriority !== NoWork && - (highestPriorityLevel === NoWork || - highestPriorityLevel > root.current.pendingWorkPriority) + rootPriority !== NoWork && + (highestPriorityLevel === NoWork || highestPriorityLevel > rootPriority) ) { - highestPriorityLevel = root.current.pendingWorkPriority; + highestPriorityLevel = rootPriority; highestPriorityRoot = root; } - // We didn't find anything to do in this root, so let's try the next one. root = root.nextScheduledRoot; } if (highestPriorityRoot !== null) { @@ -307,7 +305,7 @@ module.exports = function( // unfortunately this is it. resetContextStack(); - return cloneFiber(highestPriorityRoot.current, highestPriorityLevel); + return createWorkInProgress(highestPriorityRoot.current, emptyObject); } nextPriorityLevel = NoWork; @@ -446,22 +444,16 @@ module.exports = function( const previousPriorityContext = priorityContext; priorityContext = TaskPriority; - let firstEffect; - if (finishedWork.effectTag !== NoEffect) { - // A fiber's effect list consists only of its children, not itself. So if - // the root has an effect, we need to add it to the end of the list. The - // resulting list is the set that would belong to the root's parent, if - // it had one; that is, all the effects in the tree including the root. - if (finishedWork.lastEffect !== null) { - finishedWork.lastEffect.nextEffect = finishedWork; - firstEffect = finishedWork.firstEffect; - } else { - firstEffect = finishedWork; - } - } else { - // There is no effect on the root. - firstEffect = finishedWork.firstEffect; - } + // Update the pending work priority of the root + const remainingWorkPriority = largerPriority( + finishedWork.pendingWorkPriority, + getUpdatePriority(finishedWork), + ); + root.pendingWorkPriority = remainingWorkPriority; + + // Transfer effects from the host root onto the fiber root. + transferEffectsToParent(root, finishedWork); + const firstEffect = root.firstEffect; prepareForCommit(); @@ -559,42 +551,11 @@ module.exports = function( commitPhaseBoundaries = null; } - priorityContext = previousPriorityContext; - } - - function resetWorkPriority(workInProgress: Fiber) { - let newPriority = NoWork; - - // Check for pending update priority. This is usually null so it shouldn't - // be a perf issue. - const queue = workInProgress.updateQueue; - const tag = workInProgress.tag; - if ( - queue !== null && - // TODO: Revisit once updateQueue is typed properly to distinguish between - // update payloads for host components and update queues for composites - (tag === ClassComponent || tag === HostRoot) - ) { - newPriority = getPendingPriority(queue); - } - - // TODO: Coroutines need to visit stateNode + // Reset the fiber root's effect list. + root.firstEffect = null; + root.lastEffect = null; - // progressedChild is going to be the child set with the highest priority. - // Either it is the same as child, or it just bailed out because it choose - // not to do the work. - let child = workInProgress.progressedChild; - while (child !== null) { - // Ensure that remaining work priority bubbles up. - if ( - child.pendingWorkPriority !== NoWork && - (newPriority === NoWork || newPriority > child.pendingWorkPriority) - ) { - newPriority = child.pendingWorkPriority; - } - child = child.sibling; - } - workInProgress.pendingWorkPriority = newPriority; + priorityContext = previousPriorityContext; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -604,13 +565,11 @@ module.exports = function( // means that we don't need an additional field on the work in // progress. const current = workInProgress.alternate; - const next = completeWork(current, workInProgress); + const next = completeWork(current, workInProgress, nextPriorityLevel); const returnFiber = workInProgress.return; const siblingFiber = workInProgress.sibling; - resetWorkPriority(workInProgress); - if (next !== null) { if (__DEV__) { stopWorkTimer(workInProgress); @@ -623,36 +582,6 @@ module.exports = function( return next; } - if (returnFiber !== null) { - // 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 === null) { - returnFiber.firstEffect = workInProgress.firstEffect; - } - if (workInProgress.lastEffect !== null) { - if (returnFiber.lastEffect !== null) { - returnFiber.lastEffect.nextEffect = workInProgress.firstEffect; - } - 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 !== null) { - returnFiber.lastEffect.nextEffect = workInProgress; - } else { - returnFiber.firstEffect = workInProgress; - } - returnFiber.lastEffect = workInProgress; - } - } - if (__DEV__) { stopWorkTimer(workInProgress); } @@ -843,7 +772,7 @@ module.exports = function( } function performWork( - priorityLevel: PriorityLevel, + minPriorityLevel: PriorityLevel, deadline: Deadline | null, ) { if (__DEV__) { @@ -861,9 +790,9 @@ module.exports = function( // This outer loop exists so that we can restart the work loop after // catching an error. It also lets us flush Task work at the end of a // deferred batch. - while (priorityLevel !== NoWork && !fatalError) { + while (minPriorityLevel !== NoWork && fatalError === null) { invariant( - deadline !== null || priorityLevel < HighPriority, + deadline !== null || minPriorityLevel < HighPriority, 'Cannot perform deferred work without a deadline. This error is ' + 'likely caused by a bug in React. Please file an issue.', ); @@ -884,12 +813,12 @@ module.exports = function( null, workLoop, null, - priorityLevel, + minPriorityLevel, deadline, ); } else { try { - workLoop(priorityLevel, deadline); + workLoop(minPriorityLevel, deadline); } catch (e) { error = e; } @@ -911,7 +840,9 @@ module.exports = function( // Complete the boundary as if it rendered null. This will unmount // the failed tree. - beginFailedWork(boundary.alternate, boundary, priorityLevel); + // TODO: We should unmount with task priority so that nothing else + // can render before it's unmounted. + beginFailedWork(boundary.alternate, boundary, nextPriorityLevel); // The next unit of work is now the boundary that captured the error. // Conceptually, we're unwinding the stack. We need to unwind the @@ -935,7 +866,7 @@ module.exports = function( } // Stop performing work - priorityLevel = NoWork; + minPriorityLevel = NoWork; // If have we more work, and we're in a deferred batch, check to see // if the deadline has expired. @@ -945,7 +876,7 @@ module.exports = function( !deadlineHasExpired ) { // We have more time to do work. - priorityLevel = nextPriorityLevel; + minPriorityLevel = nextPriorityLevel; continue; } @@ -956,7 +887,7 @@ module.exports = function( case TaskPriority: // Perform work immediately by switching the priority level // and continuing the loop. - priorityLevel = nextPriorityLevel; + minPriorityLevel = nextPriorityLevel; break; case AnimationPriority: scheduleAnimationCallback(performAnimationWork); @@ -1233,6 +1164,11 @@ module.exports = function( return; } + root.pendingWorkPriority = largerPriority( + root.pendingWorkPriority, + priorityLevel, + ); + if (!root.isScheduled) { root.isScheduled = true; if (lastScheduledRoot) { @@ -1266,12 +1202,14 @@ module.exports = function( } } - let node = fiber; + let child = fiber; + let node = fiber.return; let shouldContinue = true; while (node !== null && shouldContinue) { // Walk the parent path to the root and update each node's priority. Once // we reach a node whose priority matches (and whose alternate's priority - // matches) we can exit safely knowing that the rest of the path is correct. + // matches) we can exit safely knowing that the rest of the path + // is correct. shouldContinue = false; if ( node.pendingWorkPriority === NoWork || @@ -1291,39 +1229,41 @@ module.exports = function( node.alternate.pendingWorkPriority = priorityLevel; } } - if (node.return === null) { - if (node.tag === HostRoot) { - const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, priorityLevel); - // Depending on the priority level, either perform work now or - // schedule a callback to perform work later. - switch (priorityLevel) { - case SynchronousPriority: - performWork(SynchronousPriority, null); - return; - case TaskPriority: - // TODO: If we're not already performing work, schedule a - // deferred callback. - return; - case AnimationPriority: - scheduleAnimationCallback(performAnimationWork); - return; - case HighPriority: - case LowPriority: - case OffscreenPriority: - scheduleDeferredCallback(performDeferredWork); - return; - } - } else { - if (__DEV__) { - if (fiber.tag === ClassComponent) { - warnAboutUpdateOnUnmounted(fiber.stateNode); - } + child = node; + node = node.return; + } + + if (node === null) { + if (child.tag === HostRoot) { + const root: FiberRoot = (child.stateNode: any); + scheduleRoot(root, priorityLevel); + // Depending on the priority level, either perform work now or + // schedule a callback to perform work later. + switch (priorityLevel) { + case SynchronousPriority: + performWork(SynchronousPriority, null); + return; + case TaskPriority: + // TODO: If we're not already performing work, schedule a + // deferred callback. + return; + case AnimationPriority: + scheduleAnimationCallback(performAnimationWork); + return; + case HighPriority: + case LowPriority: + case OffscreenPriority: + scheduleDeferredCallback(performDeferredWork); + return; + } + } else { + if (__DEV__) { + if (fiber.tag === ClassComponent) { + warnAboutUpdateOnUnmounted(fiber.stateNode); } - return; } + return; } - node = node.return; } } @@ -1356,6 +1296,13 @@ module.exports = function( } function scheduleErrorRecovery(fiber: Fiber) { + // The error boundary's children now have TaskPriority. Conceptually, this + // is a special case of pendingProps. + fiber.pendingWorkPriority = TaskPriority; + if (fiber.alternate) { + fiber.alternate.pendingWorkPriority = TaskPriority; + } + // Bubble the priority to the root scheduleUpdate(fiber, TaskPriority); } diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 08b19e31379d..bc591e032811 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -15,14 +15,14 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; -const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); - const { NoWork, SynchronousPriority, TaskPriority, } = require('ReactPriorityLevel'); +const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); + const invariant = require('fbjs/lib/invariant'); if (__DEV__) { var warning = require('fbjs/lib/warning'); @@ -85,65 +85,19 @@ function comparePriority(a: PriorityLevel, b: PriorityLevel): number { return a - b; } -// Ensures that a fiber has an update queue, creating a new one if needed. -// Returns the new or existing queue. -function ensureUpdateQueue(fiber: Fiber): UpdateQueue { - if (fiber.updateQueue !== null) { - // We already have an update queue. - return fiber.updateQueue; - } - - let queue; +function createUpdateQueue(): UpdateQueue { + const queue: UpdateQueue = { + first: null, + last: null, + hasForceUpdate: false, + callbackList: null, + }; if (__DEV__) { - queue = { - first: null, - last: null, - hasForceUpdate: false, - callbackList: null, - isProcessing: false, - }; - } else { - queue = { - first: null, - last: null, - hasForceUpdate: false, - callbackList: null, - }; + queue.isProcessing = false; } - - fiber.updateQueue = queue; return queue; } -// Clones an update queue from a source fiber onto its alternate. -function cloneUpdateQueue( - current: Fiber, - workInProgress: Fiber, -): UpdateQueue | null { - const currentQueue = current.updateQueue; - if (currentQueue === null) { - // The source fiber does not have an update queue. - workInProgress.updateQueue = null; - return null; - } - // If the alternate already has a queue, reuse the previous object. - const altQueue = workInProgress.updateQueue !== null - ? workInProgress.updateQueue - : {}; - altQueue.first = currentQueue.first; - altQueue.last = currentQueue.last; - - // These fields are invalid by the time we clone from current. Reset them. - altQueue.hasForceUpdate = false; - altQueue.callbackList = null; - altQueue.isProcessing = false; - - workInProgress.updateQueue = altQueue; - - return altQueue; -} -exports.cloneUpdateQueue = cloneUpdateQueue; - function cloneUpdate(update: Update): Update { return { priorityLevel: update.priorityLevel, @@ -204,6 +158,64 @@ function findInsertionPosition(queue, update): Update | null { return insertAfter; } +function ensureUpdateQueues(fiber: Fiber): [UpdateQueue, UpdateQueue | null] { + const alternateFiber = fiber.alternate; + const progressedWork = fiber.progressedWork; + + // If there are + + // Ensures that the fiber, its alternate, and the progressed work object all + // have update queues, without overwriting an existing queue. There are up to + // two distinct update queues, no more. + let queue1; + let queue2; + if (alternateFiber === null) { + queue1 = fiber.updateQueue; + queue2 = null; + if (queue1 === null) { + queue1 = fiber.updateQueue = createUpdateQueue(); + } + } else { + queue1 = fiber.updateQueue; + queue2 = alternateFiber.updateQueue; + if (queue1 === null && queue2 === null) { + queue1 = queue2 = fiber.updateQueue = alternateFiber.updateQueue = createUpdateQueue(); + } else { + if (queue1 === null) { + queue1 = fiber.updateQueue = createUpdateQueue(); + } + if (queue2 === null) { + queue2 = alternateFiber.updateQueue = createUpdateQueue(); + } + } + } + + if (progressedWork !== fiber && progressedWork !== alternateFiber) { + // We have a forked progressed work object. We need to ensure we insert + // updates into this queue, too. + // + // This should only happen when a fork is followed by a bailout. So both + // fibers should have the same queue: queue1 === queue2. + // + // An exception is if setState is called during the begin phase (render). + // In that case, the forked progressed work is about to be replaced by + // the work-in-progress. So we can ignore it. + if (queue1 === queue2) { + queue2 = progressedWork.updateQueue; + if (progressedWork.updateQueue === null) { + queue2 = progressedWork.updateQueue = createUpdateQueue(); + } + } + } + + // TODO: Refactor to avoid returning a tuple. + return [ + queue1, + // Return null if there is no alternate queue, or if its queue is the same. + queue2 !== queue1 ? queue2 : null, + ]; +} + // The work-in-progress queue is a subset of the current queue (if it exists). // We need to insert the incoming update into both lists. However, it's possible // that the correct position in one list will be different from the position in @@ -234,10 +246,8 @@ function findInsertionPosition(queue, update): Update | null { // // If the update is cloned, it returns the cloned update. function insertUpdate(fiber: Fiber, update: Update): Update | null { - const queue1 = ensureUpdateQueue(fiber); - const queue2 = fiber.alternate !== null - ? ensureUpdateQueue(fiber.alternate) - : null; + // We'll have at least one and at most two distinct update queues. + const [queue1, queue2] = ensureUpdateQueues(fiber); // Warn if an update is scheduled from inside an updater function. if (__DEV__) { @@ -274,13 +284,12 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // insertion positions because it mutates the list. insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); - if (insertBefore1 !== insertBefore2) { - // The insertion positions are different, so we need to clone the update and - // insert the clone into the alternate queue. - const update2 = cloneUpdate(update); - insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); - return update2; - } else { + // See if the insertion positions are equal. Be careful to only compare + // non-null values. + if ( + (insertBefore1 === insertBefore2 && insertBefore1 !== null) || + (insertAfter1 === insertAfter2 && insertAfter1 !== null) + ) { // The insertion positions are the same, so when we inserted into the first // queue, it also inserted into the alternate. All we need to do is update // the alternate queue's `first` and `last` pointers, in case they @@ -291,9 +300,14 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { if (insertBefore2 === null) { queue2.last = null; } + return null; + } else { + // The insertion positions are different, so we need to clone the update and + // insert the clone into the alternate queue. + const update2 = cloneUpdate(update); + insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2); + return update2; } - - return null; } function addUpdate( @@ -352,10 +366,17 @@ function addForceUpdate( } exports.addForceUpdate = addForceUpdate; -function getPendingPriority(queue: UpdateQueue): PriorityLevel { - return queue.first !== null ? queue.first.priorityLevel : NoWork; +function getUpdatePriority(fiber: Fiber): PriorityLevel { + const updateQueue = fiber.updateQueue; + if (updateQueue === null) { + return NoWork; + } + if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { + return NoWork; + } + return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork; } -exports.getPendingPriority = getPendingPriority; +exports.getUpdatePriority = getUpdatePriority; function addTopLevelUpdate( fiber: Fiber, @@ -377,13 +398,12 @@ function addTopLevelUpdate( const update2 = insertUpdate(fiber, update); if (isTopLevelUnmount) { + // TODO: Redesign the top-level mount/update/unmount API to avoid this + // special case. + const [queue1, queue2] = ensureUpdateQueues(fiber); + // Drop all updates that are lower-priority, so that the tree is not // remounted. We need to do this for both queues. - const queue1 = fiber.updateQueue; - const queue2 = fiber.alternate !== null - ? fiber.alternate.updateQueue - : null; - if (queue1 !== null && update.next !== null) { update.next = null; queue1.last = update; @@ -407,6 +427,7 @@ function getStateFromUpdate(update, instance, prevState, props) { } function beginUpdateQueue( + current: Fiber | null, workInProgress: Fiber, queue: UpdateQueue, instance: any, @@ -414,19 +435,33 @@ function beginUpdateQueue( props: any, priorityLevel: PriorityLevel, ): any { + if (current !== null && current.updateQueue === queue) { + // We need to create a work-in-progress queue, by cloning the current queue. + const currentQueue = queue; + queue = workInProgress.updateQueue = { + first: currentQueue.first, + last: currentQueue.last, + // These fields are no longer valid because they were already committed. + // Reset them. + callbackList: null, + hasForceUpdate: false, + }; + } + if (__DEV__) { // Set this flag so we can warn if setState is called inside the update // function of another setState. queue.isProcessing = true; } - queue.hasForceUpdate = false; + // Calculate these using the the existing values as a base. + let callbackList = queue.callbackList; + let hasForceUpdate = queue.hasForceUpdate; // Applies updates with matching priority to the previous state to create // a new state object. let state = prevState; let dontMutatePrevState = true; - let callbackList = queue.callbackList; let update = queue.first; while ( update !== null && @@ -456,7 +491,7 @@ function beginUpdateQueue( } } if (update.isForced) { - queue.hasForceUpdate = true; + hasForceUpdate = true; } // Second condition ignores top-level unmount callbacks if they are not the // last update in the queue, since a subsequent update will cause a remount. @@ -464,21 +499,22 @@ function beginUpdateQueue( update.callback !== null && !(update.isTopLevelUnmount && update.next !== null) ) { - callbackList = callbackList || []; + callbackList = callbackList !== null ? callbackList : []; callbackList.push(update.callback); - workInProgress.effectTag |= CallbackEffect; } update = update.next; } queue.callbackList = callbackList; + queue.hasForceUpdate = hasForceUpdate; - if (queue.first === null && callbackList === null && !queue.hasForceUpdate) { + if (queue.first === null && callbackList === null && !hasForceUpdate) { // The queue is empty and there are no callbacks. We can reset it. workInProgress.updateQueue = null; } if (__DEV__) { + // No longer processing. queue.isProcessing = false; } @@ -495,6 +531,10 @@ function commitCallbacks( if (callbackList === null) { return; } + + // Set the list to null to make sure they don't get called more than once. + queue.callbackList = null; + for (let i = 0; i < callbackList.length; i++) { const callback = callbackList[i]; invariant( diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 5bfa7066ccc2..37b4a20db463 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -493,7 +493,7 @@ describe('ReactIncremental', () => { // 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); + ReactNoop.flushDeferredPri(35); expect(ops).toEqual(['Foo', 'Bar']); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index 43aa4e4980ea..0b9ac7908edd 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -795,6 +795,172 @@ describe('ReactIncrementalSideEffects', () => { // moves to "current" without flushing due to having lower priority. Does this // even happen? Maybe a child doesn't get processed because it is lower prio? + it('does not drop priority from a progressed subtree', () => { + let ops = []; + let lowPri; + let highPri; + + function LowPriDidComplete() { + ops.push('LowPriDidComplete'); + // Because this is terminal, beginning work on LowPriDidComplete implies + // that LowPri will be completed before the scheduler yields. + return null; + } + + class LowPri extends React.Component { + state = {step: 0}; + render() { + ops.push('LowPri'); + lowPri = this; + return [ + , + , + ]; + } + } + + function LowPriSibling() { + ops.push('LowPriSibling'); + return null; + } + + class HighPri extends React.Component { + state = {step: 0}; + render() { + ops.push('HighPri'); + highPri = this; + return ; + } + } + + function App() { + ops.push('App'); + return [ +
+ + +
, +
, + ]; + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([div(span(0)), div(span(0))]); + ops = []; + + lowPri.setState({step: 1}); + // Do just enough work to begin LowPri + ReactNoop.flushDeferredPri(30); + expect(ops).toEqual(['LowPri']); + // Now we'll do one more tick of work to complete LowPri. Because LowPri + // has a sibling, the parent div of LowPri has not yet completed. + ReactNoop.flushDeferredPri(10); + expect(ops).toEqual(['LowPri', 'LowPriDidComplete']); + expect(ReactNoop.getChildren()).toEqual([ + div(span(0)), // Complete, but not yet updated + div(span(0)), + ]); + ops = []; + + // Interrupt with high pri update + ReactNoop.performAnimationWork(() => highPri.setState({step: 1})); + ReactNoop.flushAnimationPri(); + expect(ops).toEqual(['HighPri']); + expect(ReactNoop.getChildren()).toEqual([ + div(span(0)), // Completed, but not yet updated + div(span(1)), + ]); + ops = []; + + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([div(span(1)), div(span(1))]); + }); + + it('does not complete already completed work', () => { + let ops = []; + let lowPri; + let highPri; + + function LowPriDidComplete() { + ops.push('LowPriDidComplete'); + // Because this is terminal, beginning work on LowPriDidComplete implies + // that LowPri will be completed before the scheduler yields. + return null; + } + + class LowPri extends React.Component { + state = {step: 0}; + render() { + ops.push('LowPri'); + lowPri = this; + return [ + , + , + ]; + } + } + + function LowPriSibling() { + ops.push('LowPriSibling'); + return null; + } + + class HighPri extends React.Component { + state = {step: 0}; + render() { + ops.push('HighPri'); + highPri = this; + return ; + } + } + + function App() { + ops.push('App'); + return [ +
+ + +
, +
, + ]; + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([div(span(0)), div(span(0))]); + ops = []; + + lowPri.setState({step: 1}); + // Do just enough work to begin LowPri + ReactNoop.flushDeferredPri(30); + expect(ops).toEqual(['LowPri']); + // Now we'll do one more tick of work to complete LowPri. Because LowPri + // has a sibling, the parent div of LowPri has not yet completed. + ReactNoop.flushDeferredPri(10); + expect(ops).toEqual(['LowPri', 'LowPriDidComplete']); + expect(ReactNoop.getChildren()).toEqual([ + div(span(0)), // Complete, but not yet updated + div(span(0)), + ]); + ops = []; + + // Interrupt with high pri update + ReactNoop.performAnimationWork(() => highPri.setState({step: 1})); + ReactNoop.flushAnimationPri(); + expect(ops).toEqual(['HighPri']); + expect(ReactNoop.getChildren()).toEqual([ + div(span(0)), // Completed, but not yet updated + div(span(1)), + ]); + ops = []; + + // If this is not enough to commit the rest of the work, that means we're + // not bailing out on the already-completed LowPri tree. + ReactNoop.flushDeferredPri(45); + expect(ReactNoop.getChildren()).toEqual([div(span(1)), div(span(1))]); + }); + it('calls callback after update is flushed', () => { let instance; class Foo extends React.Component { diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js new file mode 100644 index 000000000000..164b8f718118 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js @@ -0,0 +1,343 @@ +/** + * 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; +var ReactFeatureFlags; + +describe('ReactIncrementalTriangle', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('ReactNoop'); + + ReactFeatureFlags = require('ReactFeatureFlags'); + ReactFeatureFlags.disableNewFiberFeatures = false; + }); + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + const FLUSH = 'FLUSH'; + function flush(unitsOfWork = Infinity) { + return { + type: FLUSH, + unitsOfWork, + }; + } + + const STEP = 'STEP'; + function step(counter) { + return { + type: STEP, + counter, + }; + } + + const INTERRUPT = 'INTERRUPT'; + function interrupt(key) { + return { + type: INTERRUPT, + }; + } + + const TOGGLE = 'TOGGLE'; + function toggle(childIndex) { + return { + type: TOGGLE, + childIndex, + }; + } + + function TriangleSimulator() { + let triangles = []; + let leafTriangles = []; + let yieldAfterEachRender = false; + class Triangle extends React.Component { + constructor(props) { + super(); + this.index = triangles.length; + triangles.push(this); + if (props.depth === 0) { + this.leafIndex = leafTriangles.length; + leafTriangles.push(this); + } + this.state = {isActive: false}; + } + activate() { + if (this.props.depth !== 0) { + throw new Error('Cannot activate non-leaf component'); + } + ReactNoop.performAnimationWork(() => { + this.setState({isActive: true}); + }); + } + deactivate() { + if (this.props.depth !== 0) { + throw new Error('Cannot deactivate non-leaf component'); + } + ReactNoop.performAnimationWork(() => { + this.setState({isActive: false}); + }); + } + shouldComponentUpdate(nextProps, nextState) { + return ( + this.props.counter !== nextProps.counter || + this.state.isActive !== nextState.isActive + ); + } + render() { + if (yieldAfterEachRender) { + ReactNoop.yield(this); + } + const {counter, depth} = this.props; + if (depth === 0) { + if (this.state.isActive) { + return ; + } + return ; + } + return [ + , + , + , + ]; + } + } + + let appInstance; + class App extends React.Component { + state = {counter: 0}; + interrupt() { + // Triggers a restart from the top. + ReactNoop.performAnimationWork(() => { + this.forceUpdate(); + }); + } + setCounter(counter) { + const currentCounter = this.state.counter; + this.setState({counter}); + return currentCounter; + } + render() { + appInstance = this; + return ; + } + } + + const depth = 3; + + let keyCounter = 0; + function reset(nextStep = 0) { + triangles = []; + leafTriangles = []; + // Remounts the whole tree by changing the key + ReactNoop.render(); + ReactNoop.flush(); + treeIsConsistent(); + return appInstance; + } + + reset(); + const totalChildren = leafTriangles.length; + const totalTriangles = triangles.length; + + function treeIsConsistent(activeTriangle, counter) { + const activeIndex = activeTriangle ? activeTriangle.leafIndex : -1; + + const children = ReactNoop.getChildren(); + for (let i = 0; i < children.length; i++) { + let child = children[i]; + let num = child.prop; + + // If an expected counter is not specified, use the value of the + // first child. + if (counter === undefined) { + if (typeof num === 'string') { + counter = num.substr(1, num.length - 2); + } else { + counter = num; + } + } + + if (i === activeIndex) { + if (num !== `*${counter}*`) { + throw new Error( + `Triangle ${i} is inconsistent: ${num} instead of *${counter}*.`, + ); + } + } else { + if (num !== counter) { + throw new Error( + `Triangle ${i} is inconsistent: ${num} instead of ${counter}.`, + ); + } + } + } + } + + function simulate(...actions) { + const app = reset(); + let expectedCounterAtEnd = app.state.counter; + + let activeTriangle = null; + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + switch (action.type) { + case FLUSH: + ReactNoop.flushUnitsOfWork(action.unitsOfWork); + break; + case STEP: + app.setCounter(action.counter); + expectedCounterAtEnd = action.counter; + break; + case INTERRUPT: + app.interrupt(); + break; + case TOGGLE: + const targetTriangle = leafTriangles[action.childIndex]; + if (targetTriangle === undefined) { + throw new Error('Target index is out of bounds'); + } + if (targetTriangle === activeTriangle) { + activeTriangle = null; + targetTriangle.deactivate(); + } else { + if (activeTriangle !== null) { + activeTriangle.deactivate(); + } + activeTriangle = targetTriangle; + targetTriangle.activate(); + } + ReactNoop.flushAnimationPri(); + break; + default: + break; + } + } + // Flush remaining work + ReactNoop.flush(); + treeIsConsistent(activeTriangle, expectedCounterAtEnd); + } + + return {simulate, totalChildren, totalTriangles}; + } + + it('works', () => { + const {simulate} = TriangleSimulator(); + simulate(step(1)); + simulate(toggle(0), step(1), toggle(0)); + simulate(step(1), toggle(0), flush(2), step(2), toggle(0)); + }); + + it('resumes work by comparing the priority at which the work-in-progress was created/updated', () => { + const {simulate} = TriangleSimulator(); + simulate( + // Start a low priority update. + step(1), + // Flush part of the tree + flush(50), + // Interrupt with a high priority update to a leaf node. Part of the + // work from the low-pri update (step 1) overlaps with this high-pri + // update, but some of it is untouched. + toggle(17), + // Start a new low priority update that is the same as the current + // value. This should override all of the previous work. The final + // value of the children should be 0. + step(0), + ); + }); + + xit('fuzz tester', () => { + // This test is not deterministic because the inputs are randomized. It runs + // a limited number of tests on every run. If it fails, it will output the + // case that led to the failure. Add the failing case to the test above + // to prevent future regressions. + const {simulate, totalTriangles, totalChildren} = TriangleSimulator(); + + const limit = 1000; + + function randomInteger(min, max) { + min = Math.ceil(min); + max = Math.floor(max); + return Math.floor(Math.random() * (max - min)) + min; + } + + function randomAction() { + switch (randomInteger(0, 4)) { + case 0: + return flush(randomInteger(0, totalTriangles * 1.5)); + case 1: + return step(randomInteger(0, 10)); + case 2: + return interrupt(); + case 3: + return toggle(randomInteger(0, totalChildren)); + default: + throw new Error('Switch statement should be exhaustive'); + } + } + + function randomActions(n) { + let actions = []; + for (let i = 0; i < n; i++) { + actions.push(randomAction()); + } + return actions; + } + + function formatActions(actions) { + let result = 'simulate('; + for (let i = 0; i < actions.length; i++) { + const action = actions[i]; + switch (action.type) { + case FLUSH: + result += `flush(${action.unitsOfWork})`; + break; + case STEP: + result += `step(${action.counter})`; + break; + case INTERRUPT: + result += 'interrupt()'; + break; + case TOGGLE: + result += `toggle(${action.childIndex})`; + break; + default: + throw new Error('Switch statement should be exhaustive'); + } + if (i !== actions.length - 1) { + result += ', '; + } + } + result += ')'; + return result; + } + + for (let i = 0; i < limit; i++) { + const actions = randomActions(5); + try { + simulate(...actions); + } catch (e) { + console.error( + ` +Triangle fuzz tester error! Copy and paste the following line into the test suite: + ${formatActions(actions)} + `, + ); + throw e; + } + } + }); +}); diff --git a/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap index 760598a4ce5c..42a093ad6340 100644 --- a/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap +++ b/src/renderers/shared/fiber/__tests__/__snapshots__/ReactIncrementalPerf-test.js.snap @@ -85,6 +85,8 @@ exports[`ReactDebugFiberPerf does not treat setState from cWM or cWRP as cascadi ⚛ (Committing Changes) ⚛ (Committing Host Effects: 2 Total) ⚛ (Calling Lifecycle Methods: 2 Total) + +⚛ (React Tree Reconciliation) " `; @@ -212,7 +214,7 @@ exports[`ReactDebugFiberPerf supports coroutines 1`] = ` ⚛ Continuation [mount] ⚛ Continuation [mount] ⚛ (Committing Changes) - ⚛ (Committing Host Effects: 3 Total) + ⚛ (Committing Host Effects: 1 Total) ⚛ (Calling Lifecycle Methods: 0 Total) " `;