diff --git a/fixtures/fiber-triangle/index.html b/fixtures/fiber-triangle/index.html index d2541eee1e93..1b5941f09b31 100644 --- a/fixtures/fiber-triangle/index.html +++ b/fixtures/fiber-triangle/index.html @@ -76,51 +76,54 @@

Fiber Example

} } - function SierpinskiTriangle({ x, y, s, children }) { - if (s <= targetSize) { - return ( - + class SierpinskiTriangle extends React.Component { + shouldComponentUpdate(nextProps) { + var o = this.props; + var n = nextProps; + return !( + o.x === n.x && + o.y === n.y && + o.s === n.s && + o.children === n.children ); - return r; } - var newSize = s / 2; - var slowDown = true; - if (slowDown) { - var e = performance.now() + 0.8; - while (performance.now() < e) { - // Artificially long execution time. + render() { + let {x, y, s, children} = this.props; + if (s <= targetSize) { + return ( + + ); + return r; + } + var newSize = s / 2; + var slowDown = true; + if (slowDown) { + var e = performance.now() + 0.8; + while (performance.now() < e) { + // Artificially long execution time. + } } - } - s /= 2; + s /= 2; - return [ - - {children} - , - - {children} - , - - {children} - , - ]; + return [ + + {children} + , + + {children} + , + + {children} + , + ]; + } } - SierpinskiTriangle.shouldComponentUpdate = function(oldProps, newProps) { - var o = oldProps; - var n = newProps; - return !( - o.x === n.x && - o.y === n.y && - o.s === n.s && - o.children === n.children - ); - }; class ExampleApplication extends React.Component { constructor() { diff --git a/src/renderers/art/ReactARTFiberEntry.js b/src/renderers/art/ReactARTFiberEntry.js index a4e340c2f75e..ca97c71e8d7e 100644 --- a/src/renderers/art/ReactARTFiberEntry.js +++ b/src/renderers/art/ReactARTFiberEntry.js @@ -532,6 +532,8 @@ const ARTRenderer = ReactFiberReconciler({ ); }, + now: ReactDOMFrameScheduling.now, + useSyncScheduling: true, }); diff --git a/src/renderers/dom/fiber/ReactDOMFiberEntry.js b/src/renderers/dom/fiber/ReactDOMFiberEntry.js index d0dd466bfb4f..01090873ad72 100644 --- a/src/renderers/dom/fiber/ReactDOMFiberEntry.js +++ b/src/renderers/dom/fiber/ReactDOMFiberEntry.js @@ -437,6 +437,8 @@ var DOMRenderer = ReactFiberReconciler({ } }, + now: ReactDOMFrameScheduling.now, + canHydrateInstance( instance: Instance | TextInstance, type: string, diff --git a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js index 799ee85c1f55..443c84b1fe83 100644 --- a/src/renderers/native-rt/ReactNativeRTFiberRenderer.js +++ b/src/renderers/native-rt/ReactNativeRTFiberRenderer.js @@ -227,6 +227,11 @@ const NativeRTRenderer = ReactFiberReconciler({ }, useSyncScheduling: true, + + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, }); module.exports = NativeRTRenderer; diff --git a/src/renderers/native/ReactNativeFiberRenderer.js b/src/renderers/native/ReactNativeFiberRenderer.js index 8f36d4886a51..859f422f4c2a 100644 --- a/src/renderers/native/ReactNativeFiberRenderer.js +++ b/src/renderers/native/ReactNativeFiberRenderer.js @@ -377,6 +377,11 @@ const NativeRenderer = ReactFiberReconciler({ }, useSyncScheduling: true, + + now(): number { + // TODO: Enable expiration by implementing this method. + return 0; + }, }); module.exports = NativeRenderer; diff --git a/src/renderers/noop/ReactNoopEntry.js b/src/renderers/noop/ReactNoopEntry.js index cc98ecb920d9..6a5ed0b11ed3 100644 --- a/src/renderers/noop/ReactNoopEntry.js +++ b/src/renderers/noop/ReactNoopEntry.js @@ -83,6 +83,8 @@ function removeChild( parentInstance.children.splice(index, 1); } +let elapsedTimeInMs = 0; + var NoopRenderer = ReactFiberReconciler({ getRootHostContext() { if (failInBeginPhase) { @@ -201,6 +203,10 @@ var NoopRenderer = ReactFiberReconciler({ prepareForCommit(): void {}, resetAfterCommit(): void {}, + + now(): number { + return elapsedTimeInMs; + }, }); var rootContainers = new Map(); @@ -336,6 +342,14 @@ var ReactNoop = { expect(actual).toEqual(expected); }, + expire(ms: number): void { + elapsedTimeInMs += ms; + }, + + flushExpired(): Array { + return ReactNoop.flushUnitsOfWork(0); + }, + yield(value: mixed) { if (yieldedValues === null) { yieldedValues = [value]; @@ -400,7 +414,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', firstUpdate && firstUpdate.partialState, firstUpdate.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); var next; while ((next = firstUpdate.next)) { @@ -408,7 +422,7 @@ var ReactNoop = { ' '.repeat(depth + 1) + '~', next.partialState, next.callback ? 'with callback' : '', - '[' + firstUpdate.priorityLevel + ']', + '[' + firstUpdate.expirationTime + ']', ); } } @@ -418,7 +432,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/ReactDOMFrameScheduling.js b/src/renderers/shared/ReactDOMFrameScheduling.js index de8d0da98f37..5a2fc01ca989 100644 --- a/src/renderers/shared/ReactDOMFrameScheduling.js +++ b/src/renderers/shared/ReactDOMFrameScheduling.js @@ -37,6 +37,20 @@ if (__DEV__) { } } +const hasNativePerformanceNow = + typeof performance === 'object' && typeof performance.now === 'function'; + +let now; +if (hasNativePerformanceNow) { + now = function() { + return performance.now(); + }; +} else { + now = function() { + return Date.now(); + }; +} + // TODO: There's no way to cancel, because Fiber doesn't atm. let rIC: (callback: (deadline: Deadline) => void) => number; @@ -67,19 +81,23 @@ if (!ExecutionEnvironment.canUseDOM) { var previousFrameTime = 33; var activeFrameTime = 33; - var frameDeadlineObject = { - timeRemaining: typeof performance === 'object' && - typeof performance.now === 'function' - ? function() { - // We assume that if we have a performance timer that the rAF callback - // gets a performance timer value. Not sure if this is always true. - return frameDeadline - performance.now(); - } - : function() { - // As a fallback we use Date.now. - return frameDeadline - Date.now(); - }, - }; + var frameDeadlineObject; + if (hasNativePerformanceNow) { + frameDeadlineObject = { + timeRemaining() { + // We assume that if we have a performance timer that the rAF callback + // gets a performance timer value. Not sure if this is always true. + return frameDeadline - performance.now(); + }, + }; + } else { + frameDeadlineObject = { + timeRemaining() { + // Fallback to Date.now() + return frameDeadline - Date.now(); + }, + }; + } // We use the postMessage trick to defer idle work until after the repaint. var messageKey = '__reactIdleCallback$' + Math.random().toString(36).slice(2); @@ -153,4 +171,5 @@ if (!ExecutionEnvironment.canUseDOM) { rIC = requestIdleCallback; } +exports.now = now; exports.rIC = rIC; diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 763ee6d9f3c9..935b897b3ca1 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -13,7 +13,7 @@ import type {ReactElement} from 'ReactElementType'; import type {ReactCoroutine, ReactPortal, ReactYield} from 'ReactTypes'; import type {Fiber} from 'ReactFiber'; -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'); @@ -288,19 +288,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; @@ -349,20 +349,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; @@ -373,21 +373,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; @@ -403,7 +403,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) { @@ -411,13 +411,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; @@ -428,21 +428,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; @@ -453,7 +453,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber: Fiber, current: Fiber | null, portal: ReactPortal, - priority: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { if ( current === null || @@ -465,13 +465,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; @@ -482,20 +482,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; @@ -505,7 +505,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 @@ -514,7 +514,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromText( '' + newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -526,7 +526,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromElement( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.ref = coerceRef(null, newChild); created.return = returnFiber; @@ -537,7 +537,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromCoroutine( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -547,7 +547,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromYield( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.type = newChild.value; created.return = returnFiber; @@ -558,7 +558,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromPortal( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -569,7 +569,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { const created = createFiberFromFragment( newChild, returnFiber.internalContextTag, - priority, + expirationTime, ); created.return = returnFiber; return created; @@ -591,7 +591,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. @@ -604,14 +604,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; } @@ -619,7 +629,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; } @@ -630,7 +645,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; } @@ -638,7 +653,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; } @@ -651,7 +671,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); @@ -671,13 +691,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) { @@ -687,7 +712,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: { @@ -695,14 +725,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: { @@ -710,13 +750,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, @@ -1304,8 +1359,6 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { // Handle object types const isObject = typeof newChild === 'object' && newChild !== null; if (isObject) { - // Support only the subset of return types that Stack supports. Treat - // everything else as empty, but log a warning. switch (newChild.$$typeof) { case REACT_ELEMENT_TYPE: return placeSingleChild( @@ -1313,7 +1366,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1323,17 +1376,16 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); - case REACT_YIELD_TYPE: return placeSingleChild( reconcileSingleYield( returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); @@ -1343,7 +1395,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ), ); } @@ -1355,7 +1407,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, '' + newChild, - priority, + expirationTime, ), ); } @@ -1365,7 +1417,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1374,7 +1426,7 @@ function ChildReconciler(shouldClone, shouldTrackSideEffects) { returnFiber, currentFirstChild, newChild, - priority, + expirationTime, ); } @@ -1446,7 +1498,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; @@ -1457,7 +1509,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 dae77d55cc79..8bbffcc48b6b 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -21,6 +21,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 { @@ -35,7 +36,9 @@ var { Fragment, } = require('ReactTypeOfWork'); -var {NoWork} = require('ReactPriorityLevel'); +var {NoWork: NoWorkPriority} = require('ReactPriorityLevel'); + +var {NoWork} = require('ReactFiberExpirationTime'); var {NoContext} = require('ReactTypeOfInternalContext'); @@ -134,8 +137,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 @@ -189,7 +193,7 @@ function FiberNode( this.firstEffect = null; this.lastEffect = null; - this.pendingWorkPriority = NoWork; + this.expirationTime = NoWork; this.alternate = null; @@ -233,7 +237,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) { @@ -270,7 +274,7 @@ exports.createWorkInProgress = function( workInProgress.lastEffect = null; } - workInProgress.pendingWorkPriority = renderPriority; + workInProgress.expirationTime = expirationTime; workInProgress.child = current.child; workInProgress.memoizedProps = current.memoizedProps; @@ -296,7 +300,7 @@ exports.createHostRootFiber = function(): Fiber { exports.createFiberFromElement = function( element: ReactElement, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { let owner = null; if (__DEV__) { @@ -310,7 +314,7 @@ exports.createFiberFromElement = function( owner, ); fiber.pendingProps = element.props; - fiber.pendingWorkPriority = priorityLevel; + fiber.expirationTime = expirationTime; if (__DEV__) { fiber._debugSource = element._source; @@ -323,24 +327,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; }; @@ -411,7 +415,7 @@ exports.createFiberFromHostInstanceForDeletion = function(): Fiber { exports.createFiberFromCoroutine = function( coroutine: ReactCoroutine, internalContextTag: TypeOfInternalContext, - priorityLevel: PriorityLevel, + expirationTime: ExpirationTime, ): Fiber { const fiber = createFiber( CoroutineComponent, @@ -420,27 +424,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, @@ -452,5 +457,5 @@ exports.largerPriority = function( p1: PriorityLevel, p2: PriorityLevel, ): PriorityLevel { - return p1 !== NoWork && (p2 === NoWork || p2 > p1) ? p1 : p2; + return p1 !== NoWorkPriority && (p2 === NoWorkPriority || p2 > p1) ? p1 : p2; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index ed81a1d77894..0627bc7f9b35 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -17,6 +17,7 @@ import type {HydrationContext} from 'ReactFiberHydrationContext'; import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var { mountChildFibersInPlace, @@ -47,7 +48,7 @@ var { YieldComponent, Fragment, } = ReactTypeOfWork; -var {NoWork, OffscreenPriority} = require('ReactPriorityLevel'); +var {NoWork, Never} = require('ReactFiberExpirationTime'); var { PerformedWork, Placement, @@ -71,8 +72,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, @@ -99,23 +108,25 @@ module.exports = function( getPriorityContext, 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 @@ -126,7 +137,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 @@ -139,7 +150,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 @@ -149,7 +160,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); } } @@ -223,7 +234,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. @@ -235,18 +246,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( @@ -318,7 +329,7 @@ module.exports = function( pushHostContainer(workInProgress, root.containerInfo); } - function updateHostRoot(current, workInProgress, priorityLevel) { + function updateHostRoot(current, workInProgress, renderExpirationTime) { pushHostRootContext(workInProgress); const updateQueue = workInProgress.updateQueue; if (updateQueue !== null) { @@ -330,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); } @@ -361,7 +372,7 @@ module.exports = function( workInProgress, workInProgress.child, element, - priorityLevel, + renderExpirationTime, ); } else { // Otherwise reset hydration state in case we aborted and resumed another @@ -377,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) { @@ -423,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; } @@ -452,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 ' + @@ -487,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 @@ -532,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 @@ -556,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, ); } @@ -589,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 @@ -621,7 +642,7 @@ module.exports = function( workInProgress, workInProgress.child, nextChildren, - priorityLevel, + renderExpirationTime, ); memoizeProps(workInProgress, nextChildren); } else { @@ -716,11 +737,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 === NoWork || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -730,16 +751,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: @@ -747,13 +776,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: @@ -768,7 +805,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) { @@ -801,8 +838,8 @@ module.exports = function( } if ( - workInProgress.pendingWorkPriority === NoWork || - workInProgress.pendingWorkPriority > priorityLevel + workInProgress.expirationTime === NoWork || + workInProgress.expirationTime > renderExpirationTime ) { return bailoutOnLowPriority(current, workInProgress); } @@ -814,11 +851,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 2e080e23bff4..62f35742b025 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -12,6 +12,7 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; var {Update} = require('ReactTypeOfSideEffect'); @@ -77,10 +78,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 = { @@ -88,32 +97,67 @@ module.exports = function( enqueueSetState(instance, partialState, 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, 'setState'); } - addUpdate(fiber, partialState, callback, priorityLevel); - 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); - 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); - scheduleUpdate(fiber, priorityLevel); + addForceUpdate( + fiber, + callback, + priorityLevel, + expirationTime, + currentTime, + ); + scheduleUpdate(fiber, expirationTime); }, }; @@ -383,7 +427,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; @@ -430,7 +474,7 @@ module.exports = function( instance, state, props, - priorityLevel, + renderExpirationTime, ); } } @@ -548,7 +592,7 @@ module.exports = function( function updateClassInstance( current: Fiber, workInProgress: Fiber, - priorityLevel: PriorityLevel, + renderExpirationTime: ExpirationTime, ): boolean { const instance = workInProgress.stateNode; resetInputPointers(workInProgress, instance); @@ -597,7 +641,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 451e379965da..edb6048e9133 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -12,7 +12,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'; @@ -25,7 +25,7 @@ var { } = require('ReactFiberContext'); var ReactTypeOfWork = require('ReactTypeOfWork'); var ReactTypeOfSideEffect = require('ReactTypeOfSideEffect'); -var ReactPriorityLevel = require('ReactPriorityLevel'); +var ReactFiberExpirationTime = require('ReactFiberExpirationTime'); var { IndeterminateComponent, FunctionalComponent, @@ -40,7 +40,7 @@ var { Fragment, } = ReactTypeOfWork; var {Placement, Ref, Update} = ReactTypeOfSideEffect; -var {OffscreenPriority} = ReactPriorityLevel; +var {Never} = ReactFiberExpirationTime; var invariant = require('fbjs/lib/invariant'); @@ -113,6 +113,7 @@ module.exports = function( function moveCoroutineToHandlerPhase( current: Fiber | null, workInProgress: Fiber, + renderExpirationTime: ExpirationTime, ) { var coroutine = (workInProgress.memoizedProps: ?ReactCoroutine); invariant( @@ -139,13 +140,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; } @@ -181,15 +180,15 @@ module.exports = function( function completeWork( current: Fiber | null, workInProgress: Fiber, - renderPriority: PriorityLevel, + renderExpirationTime: ExpirationTime, ): Fiber | null { // Get the latest props. let newProps = workInProgress.pendingProps; 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; @@ -358,7 +357,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 new file mode 100644 index 000000000000..0413f8a5994d --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberExpirationTime.js @@ -0,0 +1,123 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @providesModule ReactFiberExpirationTime + * @flow + */ + +'use strict'; + +import type {PriorityLevel} from 'ReactPriorityLevel'; +const { + NoWork: NoWorkPriority, + SynchronousPriority, + TaskPriority, + HighPriority, + LowPriority, + OffscreenPriority, +} = require('ReactPriorityLevel'); + +const invariant = require('fbjs/lib/invariant'); + +// TODO: Use an opaque type once ESLint et al support the syntax +export type ExpirationTime = number; + +const NoWork = 0; +const Sync = 1; +const Task = 2; +const Never = 2147483647; // Max int32: Math.pow(2, 31) - 1 + +const UNIT_SIZE = 10; +const MAGIC_NUMBER_OFFSET = 3; + +exports.NoWork = NoWork; +exports.Never = Never; + +// 1 unit of expiration time represents 10ms. +function msToExpirationTime(ms: number): ExpirationTime { + // Always add an offset so that we don't clash with the magic number for NoWork. + return ((ms / UNIT_SIZE) | 0) + MAGIC_NUMBER_OFFSET; +} +exports.msToExpirationTime = msToExpirationTime; + +function ceiling(num: number, precision: number): number { + return (((((num * precision) | 0) + 1) / precision) | 0) + 1; +} + +function bucket( + currentTime: ExpirationTime, + expirationInMs: number, + precisionInMs: number, +): ExpirationTime { + return ceiling( + currentTime + expirationInMs / UNIT_SIZE, + precisionInMs / UNIT_SIZE, + ); +} + +// Given the current clock time and a priority level, returns an expiration time +// that represents a point in the future by which some work should complete. +// The lower the priority, the further out the expiration time. We use rounding +// to batch like updates together. The further out the expiration time, the +// more we want to batch, so we use a larger precision when rounding. +function priorityToExpirationTime( + currentTime: ExpirationTime, + priorityLevel: PriorityLevel, +): ExpirationTime { + switch (priorityLevel) { + case NoWork: + return NoWorkPriority; + case SynchronousPriority: + return Sync; + case TaskPriority: + return Task; + case HighPriority: { + // Should complete within ~100ms. 120ms max. + return bucket(currentTime, 100, 20); + } + case LowPriority: { + // Should complete within ~1000ms. 1200ms max. + return bucket(currentTime, 1000, 200); + } + case OffscreenPriority: + return Never; + default: + invariant( + false, + 'Switch statement should be exhuastive. ' + + 'This error is likely caused by a bug in React. Please file an issue.', + ); + } +} +exports.priorityToExpirationTime = priorityToExpirationTime; + +// Given the current clock time and an expiration time, returns the +// corresponding priority level. The more time has advanced, the higher the +// priority level. +function expirationTimeToPriorityLevel( + currentTime: ExpirationTime, + expirationTime: ExpirationTime, +): PriorityLevel { + // First check for magic values + switch (expirationTime) { + case NoWorkPriority: + return NoWork; + case Sync: + return SynchronousPriority; + case Task: + return TaskPriority; + case Never: + return OffscreenPriority; + default: + break; + } + if (expirationTime <= currentTime) { + return TaskPriority; + } + // TODO: We don't currently distinguish between high and low priority. + return LowPriority; +} +exports.expirationTimeToPriorityLevel = expirationTimeToPriorityLevel; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 4876d88f4607..c672fe86a5c5 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -122,6 +122,8 @@ export type HostConfig = { prepareForCommit(): void, resetAfterCommit(): void, + now(): number, + // Optional hydration canHydrateInstance?: (instance: I | TI, type: T, props: P) => boolean, canHydrateTextInstance?: (instance: I | TI, text: string) => boolean, @@ -235,6 +237,8 @@ module.exports = function( var { scheduleUpdate, getPriorityContext, + getExpirationTimeForPriority, + recalculateCurrentTime, batchedUpdates, unbatchedUpdates, flushSync, @@ -274,6 +278,11 @@ module.exports = function( element.type.prototype != null && (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__) { @@ -284,8 +293,15 @@ module.exports = function( callback, ); } - addTopLevelUpdate(current, nextState, callback, priorityLevel); - 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 cc3800d129ea..52e6ebee2627 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -15,6 +15,7 @@ import type {FiberRoot} from 'ReactFiberRoot'; import type {HostConfig, Deadline} from 'ReactFiberReconciler'; import type {PriorityLevel} from 'ReactPriorityLevel'; import type {HydrationContext} from 'ReactFiberHydrationContext'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; export type CapturedError = { componentName: ?string, @@ -50,7 +51,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 { @@ -62,6 +63,13 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); +var { + Never, + msToExpirationTime, + priorityToExpirationTime, + expirationTimeToPriorityLevel, +} = require('ReactFiberExpirationTime'); + var {AsyncUpdates} = require('ReactTypeOfInternalContext'); var { @@ -83,7 +91,7 @@ var { ClassComponent, } = require('ReactTypeOfWork'); -var {getUpdatePriority} = require('ReactFiberUpdateQueue'); +var {getUpdateExpirationTime} = require('ReactFiberUpdateQueue'); var {resetContext} = require('ReactFiberContext'); @@ -166,6 +174,8 @@ module.exports = function( hydrationContext, scheduleUpdate, getPriorityContext, + recalculateCurrentTime, + getExpirationTimeForPriority, ); const {completeWork} = ReactFiberCompleteWork( config, @@ -181,16 +191,19 @@ module.exports = function( commitDetachRef, } = ReactFiberCommitWork(config, captureError); const { + now, scheduleDeferredCallback, useSyncScheduling, prepareForCommit, resetAfterCommit, } = config; + // Represents the current time in ms. + const startTime = now(); + let mostRecentCurrentTime: ExpirationTime = msToExpirationTime(0); + // 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. @@ -208,7 +221,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 = NoWork; // The next fiber with an effect that we're currently committing. let nextEffect: Fiber | null = null; @@ -251,14 +265,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 === NoWork ) { // Unschedule this root. nextScheduledRoot.isScheduled = false; @@ -270,7 +281,7 @@ module.exports = function( if (nextScheduledRoot === lastScheduledRoot) { nextScheduledRoot = null; lastScheduledRoot = null; - nextPriorityLevel = NoWork; + nextRenderExpirationTime = NoWork; return null; } // Continue with the next root. @@ -279,22 +290,22 @@ module.exports = function( } let root = nextScheduledRoot; - let highestPriorityRoot = null; - let highestPriorityLevel = NoWork; + let earliestExpirationRoot = null; + let earliestExpirationTime = NoWork; while (root !== null) { if ( - root.current.pendingWorkPriority !== NoWork && - (highestPriorityLevel === NoWork || - highestPriorityLevel > root.current.pendingWorkPriority) + root.current.expirationTime !== NoWork && + (earliestExpirationTime === NoWork || + 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 @@ -303,18 +314,18 @@ module.exports = function( resetContextStack(); nextUnitOfWork = createWorkInProgress( - highestPriorityRoot.current, - highestPriorityLevel, + earliestExpirationRoot.current, + earliestExpirationTime, ); - if (highestPriorityRoot !== nextRenderedTree) { + if (earliestExpirationRoot !== nextRenderedTree) { // We've switched trees. Reset the nested update counter. nestedUpdateCount = 0; - nextRenderedTree = highestPriorityRoot; + nextRenderedTree = earliestExpirationRoot; } return; } - nextPriorityLevel = NoWork; + nextRenderExpirationTime = NoWork; nextUnitOfWork = null; nextRenderedTree = null; return; @@ -392,7 +403,6 @@ module.exports = function( while (nextEffect !== null) { const effectTag = nextEffect.effectTag; - // Use Task priority for lifecycle updates if (effectTag & (Update | Callback)) { if (__DEV__) { recordEffect(); @@ -446,10 +456,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++; @@ -583,35 +590,39 @@ 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); + if ( + child.expirationTime !== NoWork && + (newExpirationTime === NoWork || + newExpirationTime > child.expirationTime) + ) { + newExpirationTime = child.expirationTime; + } child = child.sibling; } - workInProgress.pendingWorkPriority = newPriority; + workInProgress.expirationTime = newExpirationTime; } function completeUnitOfWork(workInProgress: Fiber): Fiber | null { @@ -624,7 +635,11 @@ module.exports = function( if (__DEV__) { ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - const next = completeWork(current, workInProgress, nextPriorityLevel); + const next = completeWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -632,7 +647,7 @@ module.exports = function( const returnFiber = workInProgress.return; const siblingFiber = workInProgress.sibling; - resetWorkPriority(workInProgress, nextPriorityLevel); + resetExpirationTime(workInProgress, nextRenderExpirationTime); if (next !== null) { if (__DEV__) { @@ -720,7 +735,7 @@ module.exports = function( startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - let next = beginWork(current, workInProgress, nextPriorityLevel); + let next = beginWork(current, workInProgress, nextRenderExpirationTime); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -750,7 +765,11 @@ module.exports = function( startWorkTimer(workInProgress); ReactDebugCurrentFiber.setCurrentFiber(workInProgress); } - let next = beginFailedWork(current, workInProgress, nextPriorityLevel); + let next = beginFailedWork( + current, + workInProgress, + nextRenderExpirationTime, + ); if (__DEV__) { ReactDebugCurrentFiber.resetCurrentFiber(); } @@ -785,7 +804,8 @@ module.exports = function( if ( capturedErrors !== null && capturedErrors.size > 0 && - nextPriorityLevel === TaskPriority + nextRenderExpirationTime !== NoWork && + nextRenderExpirationTime <= mostRecentCurrentTime ) { while (nextUnitOfWork !== null) { if (hasCapturedError(nextUnitOfWork)) { @@ -801,14 +821,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 === NoWork || + 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 @@ -822,28 +840,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 === NoWork || + 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) { @@ -853,24 +869,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 === NoWork || + 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); @@ -887,18 +901,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 === NoWork || + nextRenderExpirationTime > minExpirationTime || + nextRenderExpirationTime <= mostRecentCurrentTime ) { - // The priority level does not match. + // We've completed all the async work. break; } } else { @@ -913,12 +925,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; @@ -932,7 +948,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; } @@ -954,7 +973,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. @@ -970,7 +989,7 @@ module.exports = function( nextUnitOfWork = performFailedUnitOfWork(boundary); // Continue working. - workLoop(minPriorityLevel, deadline); + workLoop(minExpirationTime, deadline); } function performWork( @@ -988,21 +1007,32 @@ module.exports = function( ); isPerformingWork = true; - // 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 = getExpirationTimeForPriority( + 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; @@ -1049,7 +1079,7 @@ module.exports = function( null, failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); if (hasCaughtError()) { @@ -1062,7 +1092,7 @@ module.exports = function( performWorkCatchBlock( failedWork, boundary, - minPriorityLevel, + minExpirationTime, deadline, ); error = null; @@ -1076,19 +1106,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. @@ -1353,8 +1385,8 @@ module.exports = function( } } - function scheduleRoot(root: FiberRoot, priorityLevel: PriorityLevel) { - if (priorityLevel === NoWork) { + function scheduleRoot(root: FiberRoot, expirationTime: ExpirationTime) { + if (expirationTime === NoWork) { return; } @@ -1372,13 +1404,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__) { @@ -1396,7 +1428,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. @@ -1413,36 +1445,40 @@ 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 === NoWork || + 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 === NoWork || + 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) { + const priorityLevel = expirationTimeToPriorityLevel( + mostRecentCurrentTime, + expirationTime, + ); 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 @@ -1457,11 +1493,12 @@ module.exports = function( invariant( isBatchingUpdates, 'Task updates can only be scheduled as a nested update or ' + - 'inside batchedUpdates.', + 'inside batchedUpdates. This error is likely caused by a ' + + 'bug in React. Please file an issue.', ); break; default: - // Schedule a callback to perform the work later. + // This update is async. Schedule a callback. if (!isCallbackScheduled) { scheduleDeferredCallback(performDeferredWork); isCallbackScheduled = true; @@ -1484,7 +1521,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 ( @@ -1509,8 +1552,31 @@ 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); + const taskTime = getExpirationTimeForPriority( + mostRecentCurrentTime, + TaskPriority, + ); + scheduleUpdateImpl(fiber, taskTime, true); + } + + function recalculateCurrentTime(): ExpirationTime { + // Subtract initial time so it fits inside 32bits + const ms = now() - startTime; + mostRecentCurrentTime = msToExpirationTime(ms); + return mostRecentCurrentTime; } function batchedUpdates(fn: (a: A) => R, a: A): R { @@ -1575,6 +1641,8 @@ module.exports = function( return { scheduleUpdate: scheduleUpdate, getPriorityContext: getPriorityContext, + recalculateCurrentTime: recalculateCurrentTime, + getExpirationTimeForPriority: getExpirationTimeForPriority, batchedUpdates: batchedUpdates, unbatchedUpdates: unbatchedUpdates, flushSync: flushSync, diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index 39f483b7d78d..596ed9d41c98 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -12,14 +12,11 @@ import type {Fiber} from 'ReactFiber'; import type {PriorityLevel} from 'ReactPriorityLevel'; +import type {ExpirationTime} from 'ReactFiberExpirationTime'; const {Callback: CallbackEffect} = require('ReactTypeOfSideEffect'); -const { - NoWork, - SynchronousPriority, - TaskPriority, -} = require('ReactPriorityLevel'); +const {NoWork} = require('ReactFiberExpirationTime'); const {ClassComponent, HostRoot} = require('ReactTypeOfWork'); @@ -35,8 +32,9 @@ type PartialState = // Callbacks are not validated until invocation type Callback = mixed; -type Update = { - priorityLevel: PriorityLevel, +export type Update = { + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, partialState: PartialState, callback: Callback | null, isReplace: boolean, @@ -69,25 +67,6 @@ export type UpdateQueue = { let _queue1; let _queue2; -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, @@ -104,6 +83,7 @@ function createUpdateQueue(): UpdateQueue { function cloneUpdate(update: Update): Update { return { priorityLevel: update.priorityLevel, + expirationTime: update.expirationTime, partialState: update.partialState, callback: update.callback, isReplace: update.isReplace, @@ -113,14 +93,31 @@ function cloneUpdate(update: Update): Update { }; } +const COALESCENCE_THRESHOLD: ExpirationTime = 10; + function insertUpdateIntoQueue( queue: UpdateQueue, update: Update, insertAfter: Update | null, insertBefore: Update | null, + currentTime: ExpirationTime, ) { if (insertAfter !== null) { insertAfter.next = update; + // If we receive multiple updates to the same fiber at the same priority + // level, we coalesce them by assigning the same expiration time, so that + // 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 === update.priorityLevel + ) { + const coalescedTime = insertAfter.expirationTime; + if (coalescedTime - currentTime > COALESCENCE_THRESHOLD) { + update.expirationTime = coalescedTime; + } + } } else { // This is the first item in the queue. update.next = queue.first; @@ -138,13 +135,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; @@ -152,7 +146,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; @@ -213,7 +207,11 @@ function ensureUpdateQueues(fiber: Fiber) { // we shouldn't make a copy. // // If the update is cloned, it returns the cloned update. -function insertUpdate(fiber: Fiber, update: Update): Update | null { +function insertUpdate( + fiber: Fiber, + update: Update, + currentTime: ExpirationTime, +): Update | null { // We'll have at least one and at most two distinct update queues. ensureUpdateQueues(fiber); const queue1 = _queue1; @@ -240,7 +238,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { if (queue2 === null) { // If there's no alternate queue, there's nothing else to do but insert. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); + insertUpdateIntoQueue( + queue1, + update, + insertAfter1, + insertBefore1, + currentTime, + ); return null; } @@ -252,7 +256,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // Now we can insert into the first queue. This must come after finding both // insertion positions because it mutates the list. - insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1); + insertUpdateIntoQueue( + queue1, + update, + insertAfter1, + insertBefore1, + currentTime, + ); // See if the insertion positions are equal. Be careful to only compare // non-null values. @@ -275,7 +285,13 @@ function insertUpdate(fiber: Fiber, update: Update): Update | null { // 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); + insertUpdateIntoQueue( + queue2, + update2, + insertAfter2, + insertBefore2, + currentTime, + ); return update2; } } @@ -284,10 +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, partialState, callback, isReplace: false, @@ -295,7 +314,7 @@ function addUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addUpdate = addUpdate; @@ -303,10 +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, partialState: state, callback, isReplace: true, @@ -314,17 +336,20 @@ function addReplaceUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addReplaceUpdate = addReplaceUpdate; function addForceUpdate( fiber: Fiber, callback: Callback | null, - priorityLevel: PriorityLevel, + priorityLevel: PriorityLevel | null, + expirationTime: ExpirationTime, + currentTime: ExpirationTime, ): void { const update = { priorityLevel, + expirationTime, partialState: null, callback, isReplace: false, @@ -332,11 +357,11 @@ function addForceUpdate( isTopLevelUnmount: false, next: null, }; - insertUpdate(fiber, update); + insertUpdate(fiber, update, currentTime); } exports.addForceUpdate = addForceUpdate; -function getUpdatePriority(fiber: Fiber): PriorityLevel { +function getUpdateExpirationTime(fiber: Fiber): ExpirationTime { const updateQueue = fiber.updateQueue; if (updateQueue === null) { return NoWork; @@ -344,20 +369,23 @@ function getUpdatePriority(fiber: Fiber): PriorityLevel { if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) { return NoWork; } - return updateQueue.first !== null ? updateQueue.first.priorityLevel : NoWork; + return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork; } -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, partialState, callback, isReplace: false, @@ -365,7 +393,7 @@ function addTopLevelUpdate( isTopLevelUnmount, next: null, }; - const update2 = insertUpdate(fiber, update); + const update2 = insertUpdate(fiber, update, currentTime); if (isTopLevelUnmount) { // TODO: Redesign the top-level mount/update/unmount API to avoid this @@ -404,7 +432,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. @@ -434,10 +462,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. diff --git a/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js new file mode 100644 index 000000000000..7b9121f19a20 --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactExpiration-test.js @@ -0,0 +1,125 @@ +/** + * Copyright (c) 2013-present, Facebook, Inc. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +'use strict'; + +var React; +var ReactNoop; + +describe('ReactExpiration', () => { + beforeEach(() => { + jest.resetModules(); + React = require('react'); + ReactNoop = require('react-noop-renderer'); + }); + + function span(prop) { + return {type: 'span', children: [], prop}; + } + + it('increases priority of updates as time progresses', () => { + ReactNoop.render(); + + expect(ReactNoop.getChildren()).toEqual([]); + + // Nothing has expired yet because time hasn't advanced. + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by 300ms, not enough to expire the low pri update. + ReactNoop.expire(300); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([]); + + // Advance by another second. Now the update should expire and flush. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span('done')]); + }); + + it('coalesces updates to the same component', () => { + const foos = []; + class Foo extends React.Component { + constructor() { + super(); + this.state = {step: 0}; + foos.push(this); + } + render() { + return ; + } + } + + ReactNoop.render([, ]); + ReactNoop.flush(); + const [a, b] = foos; + + a.setState({step: 1}); + + // Advance time by 500ms. + ReactNoop.expire(500); + + // Update A again. This update should coalesce with the previous update. + a.setState({step: 2}); + // Update B. This is the first update, so it has nothing to coalesce with. + b.setState({step: 1}); + + // Advance time. This should be enough to flush both updates to A, but not + // the update to B. If only the first update to A flushes, but not the + // second, then it wasn't coalesced properly. + ReactNoop.expire(600); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2), span(0)]); + + // Now expire the update to B. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2), span(1)]); + }); + + it('stops coalescing after a certain threshold', () => { + let instance; + class Foo extends React.Component { + state = {step: 0}; + render() { + instance = this; + return ; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + + instance.setState({step: 1}); + + // Advance time by 500 ms. + ReactNoop.expire(500); + + // Update again. This update should coalesce with the previous update. + instance.setState({step: 2}); + + // Advance time by 480ms. Not enough to expire the updates. + ReactNoop.expire(480); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(0)]); + + // Update again. This update should NOT be coalesced, because the + // previous updates have almost expired. + instance.setState({step: 3}); + + // Advance time. This should expire the first two updates, + // but not the third. + ReactNoop.expire(500); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(2)]); + + // Now expire the remaining update. + ReactNoop.expire(1000); + ReactNoop.flushExpired(); + expect(ReactNoop.getChildren()).toEqual([span(3)]); + }); +}); diff --git a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js index 7c00d20f50f7..60416632715f 100644 --- a/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactFiberHostContext-test.js @@ -46,6 +46,9 @@ describe('ReactFiberHostContext', () => { appendChildToContainer: function() { return null; }, + now: function() { + return 0; + }, useSyncScheduling: true, }); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js index fdeefaa3d975..d5cdd22ae431 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalTriangle-test.js @@ -54,6 +54,14 @@ describe('ReactIncrementalTriangle', () => { }; } + const EXPIRE = 'EXPIRE'; + function expire(ms) { + return { + type: EXPIRE, + ms, + }; + } + function TriangleSimulator() { let triangles = []; let leafTriangles = []; @@ -212,6 +220,9 @@ describe('ReactIncrementalTriangle', () => { targetTriangle.activate(); } break; + case EXPIRE: + ReactNoop.expire(action.ms); + break; default: break; } @@ -251,7 +262,7 @@ describe('ReactIncrementalTriangle', () => { } function randomAction() { - switch (randomInteger(0, 4)) { + switch (randomInteger(0, 5)) { case 0: return flush(randomInteger(0, totalTriangles * 1.5)); case 1: @@ -260,6 +271,8 @@ describe('ReactIncrementalTriangle', () => { return interrupt(); case 3: return toggle(randomInteger(0, totalChildren)); + case 4: + return expire(randomInteger(0, 1500)); default: throw new Error('Switch statement should be exhaustive'); } @@ -290,6 +303,9 @@ describe('ReactIncrementalTriangle', () => { case TOGGLE: result += `toggle(${action.childIndex})`; break; + case EXPIRE: + result += `expire(${action.ms})`; + break; default: throw new Error('Switch statement should be exhaustive'); } diff --git a/src/renderers/testing/ReactTestRendererFiberEntry.js b/src/renderers/testing/ReactTestRendererFiberEntry.js index 6779c0dc3662..2a6d9e1ea576 100644 --- a/src/renderers/testing/ReactTestRendererFiberEntry.js +++ b/src/renderers/testing/ReactTestRendererFiberEntry.js @@ -247,6 +247,11 @@ var TestRenderer = ReactFiberReconciler({ useSyncScheduling: true, getPublicInstance, + + now(): number { + // Test renderer does not use expiration + return 0; + }, }); var defaultTestOptions = {