From 796db96126ea91e0c2ae3c21c25e47a44cc4b505 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Thu, 10 Aug 2017 14:48:23 -0700 Subject: [PATCH] Replace pendingWorkPriority with expiration times Instead of a priority, a fiber has an expiration time that represents a point in the future by which it should render. Pending updates still have priorities so that they can be coalesced. We use a host config method to read the current time. This commit implements everything except that method, which currently returns a constant value. So this just proves that expiration times work the same as priorities when time is frozen. Subsequent commits will show the effect of advancing time. --- src/renderers/dom/fiber/ReactDOMFiberEntry.js | 33 +- src/renderers/noop/ReactNoopEntry.js | 6 +- src/renderers/shared/fiber/ReactChildFiber.js | 215 +++++++----- src/renderers/shared/fiber/ReactFiber.js | 39 ++- .../shared/fiber/ReactFiberBeginWork.js | 130 ++++--- .../shared/fiber/ReactFiberClassComponent.js | 63 +++- .../shared/fiber/ReactFiberCompleteWork.js | 23 +- .../shared/fiber/ReactFiberExpirationTime.js | 19 +- .../shared/fiber/ReactFiberReconciler.js | 16 +- .../shared/fiber/ReactFiberScheduler.js | 324 ++++++++++-------- .../shared/fiber/ReactFiberUpdateQueue.js | 94 ++--- 11 files changed, 567 insertions(+), 395 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index 032ffa3a5685f..e2fd3916ac47f 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -164,20 +164,20 @@ function shouldAutoFocusHostComponent(type: string, props: Props): boolean { } // TODO: Better polyfill -let now; -if ( - typeof window !== 'undefined' && - window.performance && - typeof window.performance.now === 'function' -) { - now = function() { - return performance.now(); - }; -} else { - now = function() { - return Date.now(); - }; -} +// let now; +// if ( +// typeof window !== 'undefined' && +// window.performance && +// typeof window.performance.now === 'function' +// ) { +// now = function() { +// return performance.now(); +// }; +// } else { +// now = function() { +// return Date.now(); +// }; +// } var DOMRenderer = ReactFiberReconciler({ getRootHostContext(rootContainerInstance: Container): HostContext { @@ -447,7 +447,10 @@ var DOMRenderer = ReactFiberReconciler({ } }, - now: now, + now() { + // TODO: Use performance.now to enable expiration + return 0; + }, canHydrateInstance( instance: Instance | TextInstance, diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index 017e2bd572f02..ddd89bcf0ae61 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -412,7 +412,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', firstUpdate && firstUpdate.partialState, firstUpdate.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); var next; while ((next = firstUpdate.next)) { @@ -420,7 +420,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', next.partialState, next.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); } } @@ -430,7 +430,7 @@ var ReactNoop = { ' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), - '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']', + '[' + fiber.expirationTime + (fiber.pendingProps ? '*' : '') + ']', ); if (fiber.updateQueue) { logUpdateQueue(fiber.updateQueue, depth); diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 9a4157b13e90c..c51af59310140 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -16,7 +16,7 @@ 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'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {REACT_COROUTINE_TYPE, REACT_YIELD_TYPE} = require('ReactCoroutine'); var {REACT_PORTAL_TYPE} = require('ReactPortal'); @@ -285,19 +285,19 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { return existingChildren; } - function useFiber(fiber: Fiber, priority: PriorityLevel): Fiber { + function useFiber(fiber: Fiber, expirationTime: ExpirationTime): 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 = createWorkInProgress(fiber, priority); + const clone = createWorkInProgress(fiber, expirationTime); 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 + // We override the expiration time even if it is earlier, because if + // we're reconciling at a later time that means that this was // down-prioritized. - fiber.pendingWorkPriority = priority; + fiber.expirationTime = expirationTime; fiber.effectTag = NoEffect; fiber.index = 0; fiber.sibling = null; @@ -346,20 +346,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, textContent: string, - priority: PriorityLevel, + expirationTime: ExpirationTime, ) { if (current === null || current.tag !== HostText) { // Insert const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = textContent; existing.return = returnFiber; return existing; @@ -370,21 +370,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, element: ReactElement, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.type !== element.type) { // Insert const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(current, element); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.ref = coerceRef(current, element); existing.pendingProps = element.props; existing.return = returnFiber; @@ -400,7 +400,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // TODO: Should this also compare handler to determine whether to reuse? if (current === null || current.tag !== CoroutineComponent) { @@ -408,13 +408,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; @@ -425,21 +425,21 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.tag !== YieldComponent) { // Insert const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = yieldNode.value; created.return = returnFiber; return created; } else { // Move based on index - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -450,7 +450,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if ( current === null || @@ -462,13 +462,13 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; @@ -479,20 +479,20 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, fragment: Iterable<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if (current === null || current.tag !== Fragment) { // Insert const created = createFiberFromFragment( fragment, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; } else { // Update - const existing = useFiber(current, priority); + const existing = useFiber(current, expirationTime); existing.pendingProps = fragment; existing.return = returnFiber; return existing; @@ -502,7 +502,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { function createChild( returnFiber: Fiber, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { if (typeof newChild === 'string' || typeof newChild === 'number') { // Text nodes doesn't have keys. If the previous node is implicitly keyed @@ -511,7 +511,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( '' + newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -523,7 +523,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(null, newChild); created.return = returnFiber; @@ -534,7 +534,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -544,7 +544,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = newChild.value; created.return = returnFiber; @@ -555,7 +555,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -566,7 +566,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromFragment( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -589,7 +589,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, oldFiber: Fiber | null, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // Update the fiber if the keys match, otherwise return null. @@ -602,14 +602,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateTextNode(returnFiber, oldFiber, '' + newChild, priority); + return updateTextNode( + returnFiber, + oldFiber, + '' + newChild, + expirationTime, + ); } 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, + expirationTime, + ); } else { return null; } @@ -617,7 +627,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_COROUTINE_TYPE: { if (newChild.key === key) { - return updateCoroutine(returnFiber, oldFiber, newChild, priority); + return updateCoroutine( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -628,7 +643,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, expirationTime); } else { return null; } @@ -636,7 +651,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { case REACT_PORTAL_TYPE: { if (newChild.key === key) { - return updatePortal(returnFiber, oldFiber, newChild, priority); + return updatePortal( + returnFiber, + oldFiber, + newChild, + expirationTime, + ); } else { return null; } @@ -649,7 +669,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (key !== null) { return null; } - return updateFragment(returnFiber, oldFiber, newChild, priority); + return updateFragment(returnFiber, oldFiber, newChild, expirationTime); } throwOnInvalidObjectType(returnFiber, newChild); @@ -670,13 +690,18 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, newIdx: number, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): 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, + expirationTime, + ); } if (typeof newChild === 'object' && newChild !== null) { @@ -686,7 +711,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateElement(returnFiber, matchedFiber, newChild, priority); + return updateElement( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } case REACT_COROUTINE_TYPE: { @@ -694,14 +724,24 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updateCoroutine(returnFiber, matchedFiber, newChild, priority); + return updateCoroutine( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } 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, + expirationTime, + ); } case REACT_PORTAL_TYPE: { @@ -709,13 +749,23 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { existingChildren.get( newChild.key === null ? newIdx : newChild.key, ) || null; - return updatePortal(returnFiber, matchedFiber, newChild, priority); + return updatePortal( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } } if (isArray(newChild) || getIteratorFn(newChild)) { const matchedFiber = existingChildren.get(newIdx) || null; - return updateFragment(returnFiber, matchedFiber, newChild, priority); + return updateFragment( + returnFiber, + matchedFiber, + newChild, + expirationTime, + ); } throwOnInvalidObjectType(returnFiber, newChild); @@ -782,7 +832,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildren: Array<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): 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 @@ -830,7 +880,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, oldFiber, newChildren[newIdx], - priority, + expirationTime, ); if (newFiber === null) { // TODO: This breaks on empty slots like null children. That's @@ -877,7 +927,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const newFiber = createChild( returnFiber, newChildren[newIdx], - priority, + expirationTime, ); if (!newFiber) { continue; @@ -904,7 +954,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, newChildren[newIdx], - priority, + expirationTime, ); if (newFiber) { if (shouldTrackSideEffects) { @@ -941,7 +991,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChildrenIterable: Iterable<*>, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This is the same implementation as reconcileChildrenArray(), // but using the iterator instead. @@ -1005,7 +1055,12 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { } else { nextOldFiber = oldFiber.sibling; } - const newFiber = updateSlot(returnFiber, oldFiber, step.value, priority); + const newFiber = updateSlot( + returnFiber, + oldFiber, + step.value, + expirationTime, + ); 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 @@ -1048,7 +1103,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, expirationTime); if (newFiber === null) { continue; } @@ -1074,7 +1129,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, newIdx, step.value, - priority, + expirationTime, ); if (newFiber !== null) { if (shouldTrackSideEffects) { @@ -1111,7 +1166,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, textContent: string, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { // There's no need to check for keys on text nodes since we don't have a // way to define them. @@ -1119,7 +1174,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); + const existing = useFiber(currentFirstChild, expirationTime); existing.pendingProps = textContent; existing.return = returnFiber; return existing; @@ -1130,7 +1185,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( textContent, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1140,7 +1195,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, element: ReactElement, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = element.key; let child = currentFirstChild; @@ -1150,7 +1205,7 @@ 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, expirationTime); existing.ref = coerceRef(child, element); existing.pendingProps = element.props; existing.return = returnFiber; @@ -1172,7 +1227,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( element, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(currentFirstChild, element); created.return = returnFiber; @@ -1183,7 +1238,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, coroutine: ReactCoroutine, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = coroutine.key; let child = currentFirstChild; @@ -1193,7 +1248,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { if (child.key === key) { if (child.tag === CoroutineComponent) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.pendingProps = coroutine; existing.return = returnFiber; return existing; @@ -1210,7 +1265,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( coroutine, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1220,14 +1275,14 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, yieldNode: ReactYield, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): 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, expirationTime); existing.type = yieldNode.value; existing.return = returnFiber; return existing; @@ -1239,7 +1294,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( yieldNode, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = yieldNode.value; created.return = returnFiber; @@ -1250,7 +1305,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const key = portal.key; let child = currentFirstChild; @@ -1264,7 +1319,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { child.stateNode.implementation === portal.implementation ) { deleteRemainingChildren(returnFiber, child.sibling); - const existing = useFiber(child, priority); + const existing = useFiber(child, expirationTime); existing.pendingProps = portal.children || []; existing.return = returnFiber; return existing; @@ -1281,7 +1336,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( portal, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -1294,7 +1349,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, currentFirstChild: Fiber | null, newChild: any, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber | null { // This function is not recursive. // If the top level item is an array, we treat it as a set of children, @@ -1316,7 +1371,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1326,7 +1381,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); } @@ -1338,7 +1393,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1348,7 +1403,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1358,7 +1413,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1368,7 +1423,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); } @@ -1422,7 +1477,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, '' + newChild, - priority, + expirationTime, ), ); } @@ -1432,7 +1487,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1441,7 +1496,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1513,7 +1568,7 @@ exports.cloneChildFibers = function( let currentChild = workInProgress.child; let newChild = createWorkInProgress( currentChild, - currentChild.pendingWorkPriority, + currentChild.expirationTime, ); // TODO: Pass this as an argument, since it's easy to forget. newChild.pendingProps = currentChild.pendingProps; @@ -1524,7 +1579,7 @@ exports.cloneChildFibers = function( currentChild = currentChild.sibling; newChild = newChild.sibling = createWorkInProgress( currentChild, - currentChild.pendingWorkPriority, + currentChild.expirationTime, ); newChild.pendingProps = currentChild.pendingProps; newChild.return = workInProgress; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 2a07cf2dc6369..9dd36ce3289ba 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -24,6 +24,7 @@ import type {TypeOfWork} from 'ReactTypeOfWork'; import type {TypeOfInternalContext} from 'ReactTypeOfInternalContext'; import type {TypeOfSideEffect} from 'ReactTypeOfSideEffect'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {UpdateQueue} from 'ReactFiberUpdateQueue'; var { @@ -40,6 +41,8 @@ var { var {NoWork} = require('ReactPriorityLevel'); +var {Done} = require('ReactFiberExpirationTime'); + var {NoContext} = require('ReactTypeOfInternalContext'); var {NoEffect} = require('ReactTypeOfSideEffect'); @@ -137,8 +140,9 @@ export type Fiber = {| firstEffect: Fiber | null, lastEffect: Fiber | null, - // This will be used to quickly determine if a subtree has no pending changes. - pendingWorkPriority: PriorityLevel, + // Represents a time in the future by which this work should be completed. + // This is also used to quickly determine if a subtree has no pending changes. + expirationTime: ExpirationTime, // 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 @@ -192,7 +196,7 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.pendingWorkPriority = NoWork; + this.expirationTime = Done; this.alternate = null; @@ -236,7 +240,7 @@ function shouldConstruct(Component) { // This is used to create an alternate fiber to do work on. exports.createWorkInProgress = function( current: Fiber, - renderPriority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let workInProgress = current.alternate; if (workInProgress === null) { @@ -265,7 +269,7 @@ exports.createWorkInProgress = function( } else { // We already have an alternate. // Reset the effect tag. - workInProgress.effectTag = NoWork; + workInProgress.effectTag = NoEffect; // The effect list is no longer valid. workInProgress.nextEffect = null; @@ -273,7 +277,7 @@ exports.createWorkInProgress = function( workInProgress.lastEffect = null; } - workInProgress.pendingWorkPriority = renderPriority; + workInProgress.expirationTime = expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -299,7 +303,7 @@ exports.createHostRootFiber = function(): Fiber { exports.createFiberFromElement = function( element: ReactElement, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let owner = null; if (__DEV__) { @@ -313,7 +317,7 @@ exports.createFiberFromElement = function( owner, ); fiber.pendingProps = element.props; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; if (__DEV__) { fiber._debugSource = element._source; @@ -326,24 +330,24 @@ exports.createFiberFromElement = function( exports.createFiberFromFragment = function( elements: ReactFragment, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): 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; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromText = function( content: string, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostText, null, internalContextTag); fiber.pendingProps = content; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; @@ -414,7 +418,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber { exports.createFiberFromCoroutine = function( coroutine: ReactCoroutine, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber( CoroutineComponent, @@ -423,27 +427,28 @@ exports.createFiberFromCoroutine = function( ); fiber.type = coroutine.handler; fiber.pendingProps = coroutine; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromYield = function( yieldNode: ReactYield, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(YieldComponent, null, internalContextTag); + fiber.expirationTime = expirationTime; return fiber; }; exports.createFiberFromPortal = function( portal: ReactPortal, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber(HostPortal, portal.key, internalContextTag); fiber.pendingProps = portal.children || []; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; fiber.stateNode = { containerInfo: portal.containerInfo, implementation: portal.implementation, diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 43141233e3b6e..9bf8040480935 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -50,7 +50,7 @@ var { YieldComponent, Fragment, } = ReactTypeOfWork; -var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); +var {Done, Never} = require('ReactFiberExpirationTime'); var { PerformedWork, Placement, @@ -74,9 +74,16 @@ module.exports = function( config: HostConfig, hostContext: HostContext, hydrationContext: HydrationContext, - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, + getPriorityContext: ( + fiber: Fiber, + forceAsync: boolean, + ) => PriorityLevel | null, recalculateCurrentTime: () => ExpirationTime, + getExpirationTimeForPriority: ( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ) => ExpirationTime, ) { const { shouldSetTextContent, @@ -104,23 +111,24 @@ module.exports = function( memoizeProps, memoizeState, recalculateCurrentTime, + getExpirationTimeForPriority, ); + // TODO: Remove this and use reconcileChildrenAtExpirationTime directly. function reconcileChildren(current, workInProgress, nextChildren) { - const priorityLevel = workInProgress.pendingWorkPriority; - reconcileChildrenAtPriority( + reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + workInProgress.expirationTime, ); } - function reconcileChildrenAtPriority( + function reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + renderExpirationTime, ) { if (current === null) { // If this is a fresh new component that hasn't been rendered yet, we @@ -131,7 +139,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } else if (current.child === workInProgress.child) { // If the current child is the same as the work in progress, it means that @@ -144,7 +152,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } else { // If, on the other hand, it is already using a clone, that means we've @@ -154,7 +162,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } } @@ -228,7 +236,7 @@ module.exports = function( function updateClassComponent( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ) { // 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. @@ -240,18 +248,18 @@ module.exports = function( if (!workInProgress.stateNode) { // In the initial pass we might need to construct the instance. constructClassInstance(workInProgress, workInProgress.pendingProps); - mountClassInstance(workInProgress, priorityLevel); + mountClassInstance(workInProgress, renderExpirationTime); shouldUpdate = true; } else { invariant(false, 'Resuming work not yet implemented.'); // In a resume, we'll already have an instance we can reuse. - // shouldUpdate = resumeMountClassInstance(workInProgress, priorityLevel); + // shouldUpdate = resumeMountClassInstance(workInProgress, renderExpirationTime); } } else { shouldUpdate = updateClassInstance( current, workInProgress, - priorityLevel, + renderExpirationTime, ); } return finishClassComponent( @@ -308,7 +316,7 @@ module.exports = function( return workInProgress.child; } - function updateHostRoot(current, workInProgress, priorityLevel) { + function updateHostRoot(current, workInProgress, renderExpirationTime) { const root = (workInProgress.stateNode: FiberRoot); if (root.pendingContext) { pushTopLevelContextObject( @@ -333,11 +341,11 @@ module.exports = function( null, prevState, null, - priorityLevel, + renderExpirationTime, ); if (prevState === state) { // If the state is the same as before, that's a bailout because we had - // no work matching this priority. + // no work that expires at this time. resetHydrationState(); return bailoutOnAlreadyFinishedWork(current, workInProgress); } @@ -364,7 +372,7 @@ module.exports = function( workInProgress, workInProgress.child, element, - priorityLevel, + renderExpirationTime, ); } else { // Otherwise reset hydration state in case we aborted and resumed another @@ -380,7 +388,7 @@ module.exports = function( return bailoutOnAlreadyFinishedWork(current, workInProgress); } - function updateHostComponent(current, workInProgress, renderPriority) { + function updateHostComponent(current, workInProgress, renderExpirationTime) { pushHostContext(workInProgress); if (current === null) { @@ -426,13 +434,13 @@ module.exports = function( // Check the host config to see if the children are offscreen/hidden. if ( - renderPriority !== OffscreenPriority && + renderExpirationTime !== Never && !useSyncScheduling && shouldDeprioritizeSubtree(type, nextProps) ) { // Down-prioritize the children. - workInProgress.pendingWorkPriority = OffscreenPriority; - // Bailout and come back to this fiber later at OffscreenPriority. + workInProgress.expirationTime = Never; + // Bailout and come back to this fiber later. return null; } @@ -455,7 +463,11 @@ module.exports = function( return null; } - function mountIndeterminateComponent(current, workInProgress, priorityLevel) { + function mountIndeterminateComponent( + current, + workInProgress, + renderExpirationTime, + ) { invariant( current === null, 'An indeterminate component should never have mounted. This error is ' + @@ -490,7 +502,7 @@ module.exports = function( // We will invalidate the child context in finishClassComponent() right after rendering. const hasContext = pushContextProvider(workInProgress); adoptClassInstance(workInProgress, value); - mountClassInstance(workInProgress, priorityLevel); + mountClassInstance(workInProgress, renderExpirationTime); return finishClassComponent(current, workInProgress, true, hasContext); } else { // Proceed under the assumption that this is a functional component @@ -535,7 +547,11 @@ module.exports = function( } } - function updateCoroutineComponent(current, workInProgress) { + function updateCoroutineComponent( + current, + workInProgress, + renderExpirationTime, + ) { var nextCoroutine = (workInProgress.pendingProps: null | ReactCoroutine); if (hasContextChanged()) { // Normally we can bail out on props equality but if context has changed @@ -559,30 +575,29 @@ module.exports = function( } const nextChildren = nextCoroutine.children; - const priorityLevel = workInProgress.pendingWorkPriority; - // The following is a fork of reconcileChildrenAtPriority but using + // The following is a fork of reconcileChildrenAtExpirationTime but using // stateNode to store the child. if (current === null) { workInProgress.stateNode = mountChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } else if (current.child === workInProgress.child) { workInProgress.stateNode = reconcileChildFibers( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } else { workInProgress.stateNode = reconcileChildFibersInPlace( workInProgress, workInProgress.stateNode, nextChildren, - priorityLevel, + renderExpirationTime, ); } @@ -592,9 +607,12 @@ module.exports = function( return workInProgress.stateNode; } - function updatePortalComponent(current, workInProgress) { + function updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ) { 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 @@ -624,7 +642,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); memoizeProps(workInProgress, nextChildren); } else { @@ -716,11 +734,11 @@ module.exports = function( function beginWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === Done || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -734,16 +752,24 @@ module.exports = function( return mountIndeterminateComponent( current, workInProgress, - priorityLevel, + renderExpirationTime, ); case FunctionalComponent: return updateFunctionalComponent(current, workInProgress); case ClassComponent: - return updateClassComponent(current, workInProgress, priorityLevel); + return updateClassComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostRoot: - return updateHostRoot(current, workInProgress, priorityLevel); + return updateHostRoot(current, workInProgress, renderExpirationTime); case HostComponent: - return updateHostComponent(current, workInProgress, priorityLevel); + return updateHostComponent( + current, + workInProgress, + renderExpirationTime, + ); case HostText: return updateHostText(current, workInProgress); case CoroutineHandlerPhase: @@ -751,13 +777,21 @@ module.exports = function( workInProgress.tag = CoroutineComponent; // Intentionally fall through since this is now the same. case CoroutineComponent: - return updateCoroutineComponent(current, workInProgress); + return updateCoroutineComponent( + current, + workInProgress, + renderExpirationTime, + ); 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); + return updatePortalComponent( + current, + workInProgress, + renderExpirationTime, + ); case Fragment: return updateFragment(current, workInProgress); default: @@ -772,7 +806,7 @@ module.exports = function( function beginFailedWork( current: Fiber | null, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ) { // Push context providers here to avoid a push/pop context mismatch. switch (workInProgress.tag) { @@ -806,8 +840,8 @@ module.exports = function( } if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === Done || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -819,11 +853,11 @@ module.exports = function( // Unmount the current children as if the component rendered null const nextChildren = null; - reconcileChildrenAtPriority( + reconcileChildrenAtExpirationTime( current, workInProgress, nextChildren, - priorityLevel, + renderExpirationTime, ); if (workInProgress.tag === ClassComponent) { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 0692f44e349f1..4c3077ea1dc91 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -58,11 +58,18 @@ if (__DEV__) { } module.exports = function( - scheduleUpdate: (fiber: Fiber, priorityLevel: PriorityLevel) => void, - getPriorityContext: (fiber: Fiber, forceAsync: boolean) => PriorityLevel, + scheduleUpdate: (fiber: Fiber, expirationTime: ExpirationTime) => void, + getPriorityContext: ( + fiber: Fiber, + forceAsync: boolean, + ) => PriorityLevel | null, memoizeProps: (workInProgress: Fiber, props: any) => void, memoizeState: (workInProgress: Fiber, state: any) => void, recalculateCurrentTime: () => ExpirationTime, + getExpirationTimeForPriority: ( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ) => ExpirationTime, ) { // Class component state updater const updater = { @@ -71,34 +78,66 @@ module.exports = function( const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'setState'); } - addUpdate(fiber, partialState, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addUpdate( + fiber, + partialState, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'replaceState'); } - addReplaceUpdate(fiber, state, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addReplaceUpdate( + fiber, + state, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const priorityLevel = getPriorityContext(fiber, false); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); callback = callback === undefined ? null : callback; if (__DEV__) { warnOnInvalidCallback(callback, 'forceUpdate'); } - addForceUpdate(fiber, callback, priorityLevel, currentTime); - scheduleUpdate(fiber, priorityLevel); + addForceUpdate( + fiber, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, }; @@ -365,7 +404,7 @@ module.exports = function( // Invokes the mount life-cycles on a previously never rendered instance. function mountClassInstance( workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): void { const current = workInProgress.alternate; @@ -412,7 +451,7 @@ module.exports = function( instance, state, props, - priorityLevel, + renderExpirationTime, ); } } @@ -530,7 +569,7 @@ module.exports = function( function updateClassInstance( current: Fiber, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): boolean { const instance = workInProgress.stateNode; resetInputPointers(workInProgress, instance); @@ -579,7 +618,7 @@ module.exports = function( instance, oldState, newProps, - priorityLevel, + renderExpirationTime, ); } else { newState = oldState; diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index c733f036933da..2b96e99531927 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -14,7 +14,7 @@ import type {ReactCoroutine} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; -import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; import type {HostContext} from 'ReactFiberHostContext'; import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; @@ -24,7 +24,7 @@ var {reconcileChildFibers} = require('ReactChildFiber'); var {popContextProvider} = require('ReactFiberContext'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); -var ReactPriorityLevel = require('ReactPriorityLevel'); +var ReactFiberExpirationTime = require('ReactFiberExpirationTime'); var { IndeterminateComponent, FunctionalComponent, @@ -39,7 +39,7 @@ var { Fragment, } = ReactTypeOfWork; var {Placement, Ref, Update} = ReactTypeOfSideEffect; -var {OffscreenPriority} = ReactPriorityLevel; +var {Never} = ReactFiberExpirationTime; if (__DEV__) { var ReactDebugCurrentFiber = require('ReactDebugCurrentFiber'); @@ -116,6 +116,7 @@ module.exports = function( function moveCoroutineToHandlerPhase( current: Fiber | null, workInProgress: Fiber, + renderExpirationTime: ExpirationTime, ) { var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); invariant( @@ -142,13 +143,11 @@ module.exports = function( var nextChildren = fn(props, yields); var currentFirstChild = current !== null ? current.child : null; - // Inherit the priority of the returnFiber. - const priority = workInProgress.pendingWorkPriority; workInProgress.child = reconcileChildFibers( workInProgress, currentFirstChild, nextChildren, - priority, + renderExpirationTime, ); return workInProgress.child; } @@ -184,7 +183,7 @@ module.exports = function( function completeWork( current: Fiber | null, workInProgress: Fiber, - renderPriority: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { if (__DEV__) { ReactDebugCurrentFiber.setCurrentFiber(workInProgress, null); @@ -195,8 +194,8 @@ module.exports = function( if (newProps === null) { newProps = workInProgress.memoizedProps; } else if ( - workInProgress.pendingWorkPriority !== OffscreenPriority || - renderPriority === OffscreenPriority + workInProgress.expirationTime !== Never || + renderExpirationTime === Never ) { // Reset the pending props, unless this was a down-prioritization. workInProgress.pendingProps = null; @@ -363,7 +362,11 @@ module.exports = function( return null; } case CoroutineComponent: - return moveCoroutineToHandlerPhase(current, workInProgress); + return moveCoroutineToHandlerPhase( + current, + workInProgress, + renderExpirationTime, + ); case CoroutineHandlerPhase: // Reset the tag to now be a first phase coroutine. workInProgress.tag = CoroutineComponent; diff --git a/src/renderers/shared/fiber/ReactFiberExpirationTime.js b/src/renderers/shared/fiber/ReactFiberExpirationTime.js index 79d3aeb66aa57..dd62030d3b380 100644 --- a/src/renderers/shared/fiber/ReactFiberExpirationTime.js +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -30,18 +30,20 @@ export type ExpirationTime = number; const Done = 0; exports.Done = Done; +const MAGIC_NUMBER_OFFSET = 2; + const Never = Infinity; exports.Never = Infinity; // 1 unit of expiration time represents 10ms. function msToExpirationTime(ms: number): ExpirationTime { - // Always add 1 so that we don't clash with the magic number for Done. - return Math.round(ms / 10) + 1; + // Always add an offset so that we don't clash with the magic number for Done. + return Math.round(ms / 10) + MAGIC_NUMBER_OFFSET; } exports.msToExpirationTime = msToExpirationTime; function expirationTimeToMs(expirationTime: ExpirationTime): number { - return (expirationTime - 1) * 10; + return (expirationTime - MAGIC_NUMBER_OFFSET) * 10; } function ceiling(time: ExpirationTime, precision: number): ExpirationTime { @@ -62,7 +64,7 @@ function priorityToExpirationTime( return Done; case SynchronousPriority: // Return a number lower than the current time, but higher than Done. - return 1; + return MAGIC_NUMBER_OFFSET - 1; case TaskPriority: // Return the current time, so that this work completes in this batch. return currentTime; @@ -75,6 +77,7 @@ function priorityToExpirationTime( case OffscreenPriority: return Never; default: + console.log(priorityLevel); invariant( false, 'Switch statement should be exhuastive. ' + @@ -111,3 +114,11 @@ function expirationTimeToPriorityLevel( return LowPriority; } exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; + +function earlierExpirationTime( + t1: ExpirationTime, + t2: ExpirationTime, +): ExpirationTime { + return t1 !== Done && (t2 === Done || t2 > t1) ? t1 : t2; +} +exports.earlierExpirationTime = earlierExpirationTime; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index ce7ed5ba564b4..a2b49b843e2ae 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -198,6 +198,7 @@ module.exports = function( var { scheduleUpdate, getPriorityContext, + getExpirationTimeForPriority, recalculateCurrentTime, performWithPriority, batchedUpdates, @@ -238,6 +239,10 @@ module.exports = function( (element.type.prototype: any).unstable_isAsyncReactComponent === true; const priorityLevel = getPriorityContext(current, forceAsync); const currentTime = recalculateCurrentTime(); + const expirationTime = getExpirationTimeForPriority( + currentTime, + priorityLevel, + ); const nextState = {element}; callback = callback === undefined ? null : callback; if (__DEV__) { @@ -248,8 +253,15 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, priorityLevel, currentTime); - scheduleUpdate(current, priorityLevel); + addTopLevelUpdate( + current, + nextState, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(current, expirationTime); } return { diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 06552438250c3..5175b1781b4f8 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -53,7 +53,7 @@ var ReactFiberHydrationContext = require('ReactFiberHydrationContext'); var {ReactCurrentOwner} = require('ReactGlobalSharedState'); var getComponentName = require('getComponentName'); -var {createWorkInProgress, largerPriority} = require('ReactFiber'); +var {createWorkInProgress} = require('ReactFiber'); var {onCommitRoot} = require('ReactFiberDevToolsHook'); var { @@ -65,7 +65,14 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); -var {msToExpirationTime} = require('ReactFiberExpirationTime'); +var { + Done, + Never, + msToExpirationTime, + earlierExpirationTime, + priorityToExpirationTime, + expirationTimeToPriorityLevel, +} = require('ReactFiberExpirationTime'); var {AsyncUpdates} = require('ReactTypeOfInternalContext'); @@ -88,7 +95,7 @@ var { ClassComponent, } = require('ReactTypeOfWork'); -var {getUpdatePriority} = require('ReactFiberUpdateQueue'); +var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -164,6 +171,7 @@ module.exports = function( scheduleUpdate, getPriorityContext, recalculateCurrentTime, + getExpirationTimeForPriority, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -187,12 +195,10 @@ module.exports = function( } = config; // Represents the current time in ms. - let currentTime: ExpirationTime = msToExpirationTime(now()); + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(now()); // The priority level to use when scheduling an update. We use NoWork to // represent the default priority. - // TODO: Should we change this to an array instead of using the call stack? - // Might be less confusing. let priorityContext: PriorityLevel = NoWork; // Keeps track of whether we're currently in a work loop. @@ -210,7 +216,8 @@ module.exports = function( // The next work in progress fiber that we're currently working on. let nextUnitOfWork: Fiber | null = null; - let nextPriorityLevel: PriorityLevel = NoWork; + // The time at which we're currently rendering work. + let nextRenderExpirationTime: ExpirationTime = Done; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -252,14 +259,11 @@ module.exports = function( resetHostContainer(); } - // resetNextUnitOfWork mutates the current priority context. It is reset after - // after the workLoop exits, so never call resetNextUnitOfWork from outside - // the work loop. function resetNextUnitOfWork() { // Clear out roots with no more work on them, or if they have uncaught errors while ( nextScheduledRoot !== null && - nextScheduledRoot.current.pendingWorkPriority === NoWork + nextScheduledRoot.current.expirationTime === Done ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -271,7 +275,7 @@ module.exports = function( if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; - nextPriorityLevel = NoWork; + nextRenderExpirationTime = Done; return null; } // Continue with the next root. @@ -280,22 +284,22 @@ module.exports = function( } let root = nextScheduledRoot; - let highestPriorityRoot = null; - let highestPriorityLevel = NoWork; + let earliestExpirationRoot = null; + let earliestExpirationTime = Done; while (root !== null) { if ( - root.current.pendingWorkPriority !== NoWork && - (highestPriorityLevel === NoWork || - highestPriorityLevel > root.current.pendingWorkPriority) + root.current.expirationTime !== Done && + (earliestExpirationTime === Done || + earliestExpirationTime > root.current.expirationTime) ) { - highestPriorityLevel = root.current.pendingWorkPriority; - highestPriorityRoot = root; + earliestExpirationTime = root.current.expirationTime; + earliestExpirationRoot = root; } // We didn't find anything to do in this root, so let's try the next one. root = root.nextScheduledRoot; } - if (highestPriorityRoot !== null) { - nextPriorityLevel = highestPriorityLevel; + if (earliestExpirationRoot !== null) { + nextRenderExpirationTime = earliestExpirationTime; // Before we start any new work, let's make sure that we have a fresh // stack to work from. // TODO: This call is buried a bit too deep. It would be nice to have @@ -304,13 +308,13 @@ module.exports = function( resetContextStack(); nextUnitOfWork = createWorkInProgress( - highestPriorityRoot.current, - highestPriorityLevel, + earliestExpirationRoot.current, + earliestExpirationTime, ); return; } - nextPriorityLevel = NoWork; + nextRenderExpirationTime = Done; nextUnitOfWork = null; return; } @@ -387,7 +391,6 @@ module.exports = function( while (nextEffect !== null) { const effectTag = nextEffect.effectTag; - // Use Task priority for lifecycle updates if (effectTag & (Update | Callback)) { if (__DEV__) { recordEffect(); @@ -441,10 +444,7 @@ module.exports = function( 'in React. Please file an issue.', ); - if ( - nextPriorityLevel === SynchronousPriority || - nextPriorityLevel === TaskPriority - ) { + if (nextRenderExpirationTime <= mostRecentCurrentTime) { // Keep track of the number of iterations to prevent an infinite // update loop. nestedUpdateCount++; @@ -578,35 +578,36 @@ module.exports = function( commitPhaseBoundaries = null; } - // This tree is done. Reset the unit of work pointer to the next highest - // priority root. If there's no more work left, the pointer is set to null. + // This tree is done. Reset the unit of work pointer to the root that + // expires soonest. If there's no work left, the pointer is set to null. resetNextUnitOfWork(); } - function resetWorkPriority( + function resetExpirationTime( workInProgress: Fiber, - renderPriority: PriorityLevel, + renderTime: ExpirationTime, ) { - if ( - workInProgress.pendingWorkPriority !== NoWork && - workInProgress.pendingWorkPriority > renderPriority - ) { - // This was a down-prioritization. Don't bubble priority from children. + if (renderTime !== Never && workInProgress.expirationTime === Never) { + // The children of this component are hidden. Don't bubble their + // expiration times. return; } - // Check for pending update priority. - let newPriority = getUpdatePriority(workInProgress); + // Check for pending updates. + let newExpirationTime = getUpdateExpirationTime(workInProgress); // TODO: Coroutines need to visit stateNode + // Bubble up the earliest expiration time. let child = workInProgress.child; while (child !== null) { - // Ensure that remaining work priority bubbles up. - newPriority = largerPriority(newPriority, child.pendingWorkPriority); + newExpirationTime = earlierExpirationTime( + newExpirationTime, + child.expirationTime, + ); child = child.sibling; } - workInProgress.pendingWorkPriority = newPriority; + workInProgress.expirationTime = newExpirationTime; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -616,12 +617,16 @@ 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, nextPriorityLevel); + const next = completeWork( + current, + workInProgress, + nextRenderExpirationTime, + ); const returnFiber = workInProgress.return; const siblingFiber = workInProgress.sibling; - resetWorkPriority(workInProgress, nextPriorityLevel); + resetExpirationTime(workInProgress, nextRenderExpirationTime); if (next !== null) { if (__DEV__) { @@ -708,7 +713,7 @@ module.exports = function( if (__DEV__) { startWorkTimer(workInProgress); } - let next = beginWork(current, workInProgress, nextPriorityLevel); + let next = beginWork(current, workInProgress, nextRenderExpirationTime); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress); } @@ -737,7 +742,11 @@ module.exports = function( if (__DEV__) { startWorkTimer(workInProgress); } - let next = beginFailedWork(current, workInProgress, nextPriorityLevel); + let next = beginFailedWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__ && ReactFiberInstrumentation.debugTool) { ReactFiberInstrumentation.debugTool.onBeginWork(workInProgress); } @@ -772,7 +781,8 @@ module.exports = function( if ( capturedErrors !== null && capturedErrors.size > 0 && - nextPriorityLevel === TaskPriority + nextRenderExpirationTime !== Done && + nextRenderExpirationTime <= mostRecentCurrentTime ) { while (nextUnitOfWork !== null) { if (hasCapturedError(nextUnitOfWork)) { @@ -788,14 +798,12 @@ module.exports = function( 'a bug in React. Please file an issue.', ); // We just completed a root. Commit it now. - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; - if ( capturedErrors === null || capturedErrors.size === 0 || - nextPriorityLevel !== TaskPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > mostRecentCurrentTime ) { // There are no more unhandled errors. We can exit this special // work loop. If there's still additional work, we'll perform it @@ -809,28 +817,26 @@ module.exports = function( } function workLoop( - minPriorityLevel: PriorityLevel, + minExpirationTime: ExpirationTime, deadline: Deadline | null, ) { if (pendingCommit !== null) { - priorityContext = TaskPriority; commitAllWork(pendingCommit); handleCommitPhaseErrors(); } else if (nextUnitOfWork === null) { resetNextUnitOfWork(); } - if (nextPriorityLevel === NoWork || nextPriorityLevel > minPriorityLevel) { + if ( + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime + ) { return; } - // During the render phase, updates should have the same priority at which - // we're rendering. - priorityContext = nextPriorityLevel; - loop: do { - if (nextPriorityLevel <= TaskPriority) { - // Flush all synchronous and task work. + if (nextRenderExpirationTime <= mostRecentCurrentTime) { + // Flush all expired work. while (nextUnitOfWork !== null) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); if (nextUnitOfWork === null) { @@ -840,24 +846,22 @@ module.exports = function( 'a bug in React. Please file an issue.', ); // We just completed a root. Commit it now. - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; // Clear any errors that were scheduled during the commit phase. handleCommitPhaseErrors(); - // The priority level may have changed. Check again. + // The render time may have changed. Check again. if ( - nextPriorityLevel === NoWork || - nextPriorityLevel > minPriorityLevel || - nextPriorityLevel > TaskPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime > mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the expired work. break; } } } } else if (deadline !== null) { - // Flush asynchronous work until the deadline expires. + // Flush asynchronous work until the deadline runs out of time. while (nextUnitOfWork !== null && !deadlineHasExpired) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork); @@ -874,18 +878,16 @@ module.exports = function( // We just completed a root. If we have time, commit it now. // Otherwise, we'll commit it in the next frame. if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { - priorityContext = TaskPriority; commitAllWork(pendingCommit); - priorityContext = nextPriorityLevel; // Clear any errors that were scheduled during the commit phase. handleCommitPhaseErrors(); - // The priority level may have changed. Check again. + // The render time may have changed. Check again. if ( - nextPriorityLevel === NoWork || - nextPriorityLevel > minPriorityLevel || - nextPriorityLevel < HighPriority + nextRenderExpirationTime === Done || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime <= mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the async work. break; } } else { @@ -900,12 +902,16 @@ module.exports = function( // There might be work left. Depending on the priority, we should // either perform it now or schedule a callback to perform it later. - switch (nextPriorityLevel) { + const currentTime = recalculateCurrentTime(); + switch (expirationTimeToPriorityLevel( + currentTime, + nextRenderExpirationTime, + )) { case SynchronousPriority: case TaskPriority: // We have remaining synchronous or task work. Keep performing it, // regardless of whether we're inside a callback. - if (nextPriorityLevel <= minPriorityLevel) { + if (nextRenderExpirationTime <= minExpirationTime) { continue loop; } break loop; @@ -919,7 +925,10 @@ module.exports = function( break loop; } // We are inside a callback. - if (!deadlineHasExpired && nextPriorityLevel <= minPriorityLevel) { + if ( + !deadlineHasExpired && + nextRenderExpirationTime <= minExpirationTime + ) { // We still have time. Keep working. continue loop; } @@ -941,7 +950,7 @@ module.exports = function( function performWorkCatchBlock( failedWork: Fiber, boundary: Fiber, - minPriorityLevel: PriorityLevel, + minExpirationTime: ExpirationTime, deadline: Deadline | null, ) { // We're going to restart the error boundary that captured the error. @@ -957,7 +966,7 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } function performWork( @@ -975,23 +984,32 @@ module.exports = function( ); isPerformingWork = true; - nestedUpdateCount = 0; - - // The priority context changes during the render phase. We'll need to - // reset it at the end. + // Updates that occur during the commit phase should have task priority + // by default. (Render phase updates are special; getPriorityContext + // accounts for their behavior.) const previousPriorityContext = priorityContext; + priorityContext = TaskPriority; + + // Read the current time from the host environment. + const currentTime = recalculateCurrentTime(); + const minExpirationTime = priorityToExpirationTime( + currentTime, + minPriorityLevel, + ); + + nestedUpdateCount = 0; let didError = false; let error = null; if (__DEV__) { - invokeGuardedCallback(null, workLoop, null, minPriorityLevel, deadline); + invokeGuardedCallback(null, workLoop, null, minExpirationTime, deadline); if (hasCaughtError()) { didError = true; error = clearCaughtError(); } } else { try { - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } catch (e) { didError = true; error = e; @@ -1038,7 +1056,7 @@ module.exports = function( null, failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); if (hasCaughtError()) { @@ -1051,7 +1069,7 @@ module.exports = function( performWorkCatchBlock( failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); error = null; @@ -1065,19 +1083,21 @@ module.exports = function( break; } - // Reset the priority context to its previous value. - priorityContext = previousPriorityContext; - // If we're inside a callback, set this to false, since we just flushed it. if (deadline !== null) { isCallbackScheduled = false; } // If there's remaining async work, make sure we schedule another callback. - if (nextPriorityLevel > TaskPriority && !isCallbackScheduled) { + if ( + nextRenderExpirationTime > mostRecentCurrentTime && + !isCallbackScheduled + ) { scheduleDeferredCallback(performDeferredWork); isCallbackScheduled = true; } + priorityContext = previousPriorityContext; + const errorToThrow = firstUncaughtError; // We're done performing work. Time to clean up. @@ -1339,8 +1359,8 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) { - if (priorityLevel === NoWork) { + function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (expirationTime === Done) { return; } @@ -1358,13 +1378,13 @@ module.exports = function( } } - function scheduleUpdate(fiber: Fiber, priorityLevel: PriorityLevel) { - return scheduleUpdateImpl(fiber, priorityLevel, false); + function scheduleUpdate(fiber: Fiber, expirationTime: ExpirationTime) { + return scheduleUpdateImpl(fiber, expirationTime, false); } function scheduleUpdateImpl( fiber: Fiber, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, isErrorRecovery: boolean, ) { if (__DEV__) { @@ -1382,7 +1402,7 @@ module.exports = function( ); } - if (!isPerformingWork && priorityLevel <= nextPriorityLevel) { + if (!isPerformingWork && expirationTime <= nextRenderExpirationTime) { // We must reset the current unit of work pointer so that we restart the // search from the root during the next tick, in case there is now higher // priority work somewhere earlier than before. @@ -1399,59 +1419,58 @@ module.exports = function( let node = fiber; 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. + // Walk the parent path to the root and update each node's expiration + // time. Once we reach a node whose expiration matches (and whose + // alternate's expiration matches) we can exit safely knowing that the + // rest of the path is correct. shouldContinue = false; if ( - node.pendingWorkPriority === NoWork || - node.pendingWorkPriority > priorityLevel + node.expirationTime === Done || + node.expirationTime > expirationTime ) { - // Priority did not match. Update and keep going. + // Expiration time did not match. Update and keep going. shouldContinue = true; - node.pendingWorkPriority = priorityLevel; + node.expirationTime = expirationTime; } if (node.alternate !== null) { if ( - node.alternate.pendingWorkPriority === NoWork || - node.alternate.pendingWorkPriority > priorityLevel + node.alternate.expirationTime === Done || + node.alternate.expirationTime > expirationTime ) { - // Priority did not match. Update and keep going. + // Expiration time did not match. Update and keep going. shouldContinue = true; - node.alternate.pendingWorkPriority = priorityLevel; + node.alternate.expirationTime = expirationTime; } } if (node.return === null) { if (node.tag === HostRoot) { const root: FiberRoot = (node.stateNode: any); - scheduleRoot(root, priorityLevel); + scheduleRoot(root, expirationTime); if (!isPerformingWork) { - switch (priorityLevel) { - case SynchronousPriority: - // Perform this update now. - if (isUnbatchingUpdates) { - // We're inside unbatchedUpdates, which is inside either - // batchedUpdates or a lifecycle. We should only flush - // synchronous work, not task work. - performWork(SynchronousPriority, null); - } else { - // Flush both synchronous and task work. - performWork(TaskPriority, null); - } - break; - case TaskPriority: - invariant( - isBatchingUpdates, - 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates.', - ); - break; - default: - // Schedule a callback to perform the work later. - if (!isCallbackScheduled) { - scheduleDeferredCallback(performDeferredWork); - isCallbackScheduled = true; - } + if (expirationTime < mostRecentCurrentTime) { + // This update is synchronous. Perform it now. + if (isUnbatchingUpdates) { + // We're inside unbatchedUpdates, which is inside either + // batchedUpdates or a lifecycle. We should only flush + // synchronous work, not task work. + performWork(SynchronousPriority, null); + } else { + // Flush both synchronous and task work. + performWork(TaskPriority, null); + } + } else if (expirationTime === mostRecentCurrentTime) { + invariant( + isBatchingUpdates, + 'Task updates can only be scheduled as a nested update or ' + + 'inside batchedUpdates. This error is likely caused by a ' + + 'bug in React. Please file an issue.', + ); + } else { + // This update is async. Schedule a callback. + if (!isCallbackScheduled) { + scheduleDeferredCallback(performDeferredWork); + isCallbackScheduled = true; + } } } } else { @@ -1470,7 +1489,13 @@ module.exports = function( function getPriorityContext( fiber: Fiber, forceAsync: boolean, - ): PriorityLevel { + ): PriorityLevel | null { + if (isPerformingWork && !isCommitting) { + // Updates during the render phase should expire at the same time as + // the work that is being rendered. Return null to indicate. + return null; + } + let priorityLevel = priorityContext; if (priorityLevel === NoWork) { if ( @@ -1495,13 +1520,29 @@ module.exports = function( return priorityLevel; } + function getExpirationTimeForPriority( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel | null, + ): ExpirationTime { + if (priorityLevel === null) { + // A priorityLevel of null indicates that this update should expire at + // the same time as whatever is currently being rendered. + return nextRenderExpirationTime; + } + return priorityToExpirationTime(currentTime, priorityLevel); + } + function scheduleErrorRecovery(fiber: Fiber) { - scheduleUpdateImpl(fiber, TaskPriority, true); + scheduleUpdateImpl( + fiber, + priorityToExpirationTime(mostRecentCurrentTime, TaskPriority), + true, + ); } function recalculateCurrentTime(): ExpirationTime { - currentTime = msToExpirationTime(now()); - return currentTime; + mostRecentCurrentTime = msToExpirationTime(now()); + return mostRecentCurrentTime; } function performWithPriority(priorityLevel: PriorityLevel, fn: Function) { @@ -1577,6 +1618,7 @@ module.exports = function( scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, recalculateCurrentTime: recalculateCurrentTime, + getExpirationTimeForPriority: getExpirationTimeForPriority, performWithPriority: performWithPriority, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index d3e07d115a14c..ef29ebd9319b9 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -18,13 +18,7 @@ import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); -const { - NoWork, - SynchronousPriority, - TaskPriority, -} = require('ReactPriorityLevel'); - -const {Done, priorityToExpirationTime} = require('ReactFiberExpirationTime'); +const {Done} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); @@ -41,7 +35,7 @@ type PartialState = type Callback = mixed; export type Update = { - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, @@ -72,25 +66,6 @@ export type UpdateQueue = { isProcessing?: boolean, }; -function comparePriority(a: PriorityLevel, b: PriorityLevel): number { - // When comparing update priorities, treat sync and Task work as equal. - // TODO: Could we avoid the need for this by always coercing sync priority - // to Task when scheduling an update? - if ( - (a === TaskPriority || a === SynchronousPriority) && - (b === TaskPriority || b === SynchronousPriority) - ) { - return 0; - } - if (a === NoWork && b !== NoWork) { - return -255; - } - if (a !== NoWork && b === NoWork) { - return 255; - } - return a - b; -} - function createUpdateQueue(): UpdateQueue { const queue: UpdateQueue = { first: null, @@ -126,9 +101,6 @@ function insertUpdateIntoQueue( insertBefore: Update | null, currentTime: ExpirationTime, ) { - const priorityLevel = update.priorityLevel; - - let coalescedTime: ExpirationTime | null = null; if (insertAfter !== null) { insertAfter.next = update; // If we receive multiple updates to the same fiber at the same priority @@ -136,10 +108,13 @@ function insertUpdateIntoQueue( // they all flush at the same time. Because this causes an interruption, it // could lead to starvation, so we stop coalescing once the time until the // expiration time reaches a certain threshold. - if (insertAfter !== null && insertAfter.priorityLevel === priorityLevel) { - const expirationTime = insertAfter.expirationTime; - if (expirationTime - currentTime > COALESCENCE_THRESHOLD) { - coalescedTime = expirationTime; + if ( + insertAfter !== null && + insertAfter.priorityLevel === update.priorityLevel + ) { + const coalescedTime = insertAfter.expirationTime; + if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) { + update.expirationTime = coalescedTime; } } } else { @@ -147,11 +122,6 @@ function insertUpdateIntoQueue( update.next = queue.first; queue.first = update; } - update.expirationTime = coalescedTime !== null - ? coalescedTime - : // If we don't coalesce, calculate the expiration time using the - // current time. - priorityToExpirationTime(currentTime, priorityLevel); if (insertBefore !== null) { update.next = insertBefore; @@ -164,13 +134,10 @@ function insertUpdateIntoQueue( // Returns the update after which the incoming update should be inserted into // the queue, or null if it should be inserted at beginning. function findInsertionPosition(queue, update): Update | null { - const priorityLevel = update.priorityLevel; + const expirationTime = update.expirationTime; let insertAfter = null; let insertBefore = null; - if ( - queue.last !== null && - comparePriority(queue.last.priorityLevel, priorityLevel) <= 0 - ) { + if (queue.last !== null && queue.last.expirationTime <= expirationTime) { // Fast path for the common case where the update should be inserted at // the end of the queue. insertAfter = queue.last; @@ -178,7 +145,7 @@ function findInsertionPosition(queue, update): Update | null { insertBefore = queue.first; while ( insertBefore !== null && - comparePriority(insertBefore.priorityLevel, priorityLevel) <= 0 + insertBefore.expirationTime <= expirationTime ) { insertAfter = insertBefore; insertBefore = insertBefore.next; @@ -333,12 +300,13 @@ function addUpdate( fiber: Fiber, partialState: PartialState | null, callback: mixed, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState, callback, isReplace: false, @@ -354,12 +322,13 @@ function addReplaceUpdate( fiber: Fiber, state: any | null, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState: state, callback, isReplace: true, @@ -374,12 +343,13 @@ exports.addReplaceUpdate = addReplaceUpdate; function addForceUpdate( fiber: Fiber, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState: null, callback, isReplace: false, @@ -391,30 +361,31 @@ function addForceUpdate( } exports.addForceUpdate = addForceUpdate; -function getUpdatePriority(fiber: Fiber): PriorityLevel { +function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; if (updateQueue === null) { - return NoWork; + return Done; } if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { - return NoWork; + return Done; } - return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork; + return updateQueue.first !== null ? updateQueue.first.expirationTime : Done; } -exports.getUpdatePriority = getUpdatePriority; +exports.getUpdateExpirationTime = getUpdateExpirationTime; function addTopLevelUpdate( fiber: Fiber, partialState: PartialState, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, currentTime: ExpirationTime, ): void { const isTopLevelUnmount = partialState.element === null; const update = { priorityLevel, - expirationTime: Done, + expirationTime, partialState, callback, isReplace: false, @@ -460,7 +431,7 @@ function beginUpdateQueue( instance: any, prevState: any, props: any, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): any { if (current !== null && current.updateQueue === queue) { // We need to create a work-in-progress queue, by cloning the current queue. @@ -490,10 +461,7 @@ function beginUpdateQueue( let state = prevState; let dontMutatePrevState = true; let update = queue.first; - while ( - update !== null && - comparePriority(update.priorityLevel, priorityLevel) <= 0 - ) { + while (update !== null && update.expirationTime <= renderExpirationTime) { // Remove each update from the queue right before it is processed. That way // if setState is called from inside an updater function, the new update // will be inserted in the correct position.