diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 0dd16c38a0..b7a4d90255 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -193,5 +193,6 @@ exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine, priority exports.createFiberFromYield = function(yieldNode : ReactYield, priorityLevel : PriorityLevel) { const fiber = createFiber(YieldComponent, yieldNode.key); + fiber.pendingProps = {}; return fiber; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 192a77e4ba..263c5d4899 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -28,6 +28,7 @@ var { YieldComponent, } = ReactTypeOfWork; var { + NoWork, OffscreenPriority, } = require('ReactPriorityLevel'); @@ -47,6 +48,7 @@ function updateFunctionalComponent(current, workInProgress) { console.log('update fn:', fn.name); var nextChildren = fn(props); reconcileChildren(current, workInProgress, nextChildren); + workInProgress.pendingWorkPriority = NoWork; } function updateHostComponent(current, workInProgress) { @@ -65,6 +67,7 @@ function updateHostComponent(current, workInProgress) { nextChildren, OffscreenPriority ); + workInProgress.pendingWorkPriority = OffscreenPriority; return null; } else { workInProgress.child = ReactChildFiber.reconcileChildFibers( @@ -73,6 +76,7 @@ function updateHostComponent(current, workInProgress) { nextChildren, priority ); + workInProgress.pendingWorkPriority = NoWork; return workInProgress.child; } } @@ -85,12 +89,19 @@ function mountIndeterminateComponent(current, workInProgress) { console.log('performed work on class:', fn.name); // Proceed under the assumption that this is a class instance workInProgress.tag = ClassComponent; + if (workInProgress.alternate) { + workInProgress.alternate.tag = ClassComponent; + } } else { console.log('performed work on fn:', fn.name); // Proceed under the assumption that this is a functional component workInProgress.tag = FunctionalComponent; + if (workInProgress.alternate) { + workInProgress.alternate.tag = FunctionalComponent; + } } reconcileChildren(current, workInProgress, value); + workInProgress.pendingWorkPriority = NoWork; } function updateCoroutineComponent(current, workInProgress) { @@ -100,6 +111,7 @@ function updateCoroutineComponent(current, workInProgress) { } console.log('begin coroutine', workInProgress.type.name); reconcileChildren(current, workInProgress, coroutine.children); + workInProgress.pendingWorkPriority = NoWork; } function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { @@ -114,6 +126,7 @@ function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { workInProgress.output = current.output; workInProgress.child = current.child; workInProgress.stateNode = current.stateNode; + workInProgress.pendingWorkPriority = NoWork; return null; } if (workInProgress.pendingProps === workInProgress.memoizedProps) { @@ -135,6 +148,7 @@ function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { reconcileChildren(current, workInProgress, workInProgress.pendingProps); // A yield component is just a placeholder, we can just run through the // next one immediately. + workInProgress.pendingWorkPriority = NoWork; if (workInProgress.child) { return beginWork( workInProgress.child.alternate, @@ -162,6 +176,7 @@ function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { case YieldComponent: // A yield component is just a placeholder, we can just run through the // next one immediately. + workInProgress.pendingWorkPriority = NoWork; if (workInProgress.sibling) { return beginWork( workInProgress.sibling.alternate, diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 5a60767613..275570c79d 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -65,7 +65,7 @@ module.exports = function(config : HostConfig) : Reconciler { let currentRootsWithPendingWork : ?Fiber = null; - function findNextUnitOfWork(priorityLevel : PriorityLevel) : ?Fiber { + function findNextUnitOfWorkAtPriority(priorityLevel : PriorityLevel) : ?Fiber { let current = currentRootsWithPendingWork; while (current) { if (current.pendingWorkPriority !== 0 && @@ -74,11 +74,15 @@ module.exports = function(config : HostConfig) : Reconciler { if (current.pendingProps !== null) { // We found some work to do. We need to return the "work in progress" // of this node which will be the alternate. - return current.alternate; + const clone = ReactFiber.cloneFiber(current, current.pendingWorkPriority); + clone.pendingProps = current.pendingProps; + return clone; } // If we have a child let's see if any of our children has work to do. // Only bother doing this at all if the current priority level matches // because it is the highest priority for the whole subtree. + // TODO: Coroutines can have work in their stateNode which is another + // type of child that needs to be searched for work. if (current.child) { current = current.child; continue; @@ -106,6 +110,21 @@ module.exports = function(config : HostConfig) : Reconciler { return null; } + function findNextUnitOfWork() { + // Find the highest possible priority work to do. + // This loop is unrolled just to satisfy Flow's enum constraint. + // We could make arbitrary many idle priority levels but having + // too many just means flushing changes too often. + let work = findNextUnitOfWorkAtPriority(HighPriority); + if (!work) { + work = findNextUnitOfWorkAtPriority(LowPriority); + if (!work) { + work = findNextUnitOfWorkAtPriority(OffscreenPriority); + } + } + return work; + } + function completeUnitOfWork(workInProgress : Fiber) : ?Fiber { while (true) { // The current, flushed, state of this fiber is the alternate. @@ -114,29 +133,62 @@ module.exports = function(config : HostConfig) : Reconciler { // progress. const current = workInProgress.alternate; const next = completeWork(current, workInProgress); + + // The work is now done. We don't need this anymore. This flags + // to the system not to redo any work here. + workInProgress.pendingProps = null; + + // TODO: Stop using the parent for this purpose. I think this will break + // down in edge cases because when nodes are reused during bailouts, we + // don't know which of two parents was used. Instead we should maintain + // a temporary manual stack. + // $FlowFixMe: This downcast is not safe. It is intentionally an error. + const parent = workInProgress.parent; + + // Ensure that remaining work priority bubbles up. + if (parent && workInProgress.pendingWorkPriority !== NoWork && + (parent.pendingWorkPriority === NoWork || + parent.pendingWorkPriority > workInProgress.pendingWorkPriority)) { + parent.pendingWorkPriority = workInProgress.pendingWorkPriority; + } + if (next) { // If completing this work spawned new work, do that next. return next; } else if (workInProgress.sibling) { // If there is more work to do in this parent, do that next. return workInProgress.sibling; - } else if (workInProgress.parent) { + } else if (parent) { // If there's no more work in this parent. Complete the parent. - // TODO: Stop using the parent for this purpose. I think this will break - // down in edge cases because when nodes are reused during bailouts, we - // don't know which of two parents was used. Instead we should maintain - // a temporary manual stack. - // $FlowFixMe: This downcast is not safe. It is intentionally an error. - workInProgress = workInProgress.parent; + workInProgress = parent; } else { // If we're at the root, there's no more work to do. - currentRootsWithPendingWork = null; - return null; + console.log('completed flush with remaining work at priority', workInProgress.pendingWorkPriority); + if (workInProgress.pendingWorkPriority !== NoWork) { + // TODO: This removes all but one node. Broken. + currentRootsWithPendingWork = workInProgress; + const nextWork = findNextUnitOfWork(); + if (!nextWork) { + // Something went wrong and there wasn't actually any more work. + // TODO: This currently fails because of the parent/root hacks. + // throw new Error('Should never finish with a priority level unless there is work.'); + currentRootsWithPendingWork = null; + return null; + } + return nextWork; + } else { + currentRootsWithPendingWork = null; + return null; + } } } } function performUnitOfWork(workInProgress : Fiber) : ?Fiber { + // Ignore work if there is nothing to do. + if (workInProgress.pendingProps === null) { + return null; + } // The current, flushed, state of this fiber is the alternate. // Ideally nothing should rely on this, but relying on it here // means that we don't need an additional field on the work in @@ -154,17 +206,7 @@ module.exports = function(config : HostConfig) : Reconciler { function performLowPriWork(deadline : Deadline) { if (!nextUnitOfWork) { - // Find the highest possible priority work to do. - // This loop is unrolled just to satisfy Flow's enum constraint. - // We could make arbitrary many idle priority levels but having - // too many just means flushing changes too often. - nextUnitOfWork = findNextUnitOfWork(HighPriority); - if (!nextUnitOfWork) { - nextUnitOfWork = findNextUnitOfWork(LowPriority); - if (!nextUnitOfWork) { - nextUnitOfWork = findNextUnitOfWork(OffscreenPriority); - } - } + nextUnitOfWork = findNextUnitOfWork(); } while (nextUnitOfWork) { if (deadline.timeRemaining() > timeHeuristicForUnitOfWork) { @@ -236,7 +278,7 @@ module.exports = function(config : HostConfig) : Reconciler { }, unmountContainer(container : OpaqueNode) : void { - container.pendingProps = null; + container.pendingProps = []; container.pendingWorkPriority = LowPriority; scheduleLowPriWork(container); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 224c44dfbf..8b6f537822 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -161,7 +161,56 @@ describe('ReactIncremental', function() { expect(ops).toEqual(['Foo', 'Bar', 'Bar']); - // TODO: Test the ability for a subtree to resume if it has lower priority. + }); + + it('can deprioritize unfinished work and resume it later', function() { + + var ops = []; + + function Bar(props) { + ops.push('Bar'); + return
{props.children}
; + } + + function Middle(props) { + ops.push('Middle'); + return {props.children}; + } + + function Foo(props) { + ops.push('Foo'); + return ( +
+ {props.text} + + {props.text} +
+ ); + } + + // Init + ReactNoop.render(); + ReactNoop.flush(); + + ops = []; + + // Render part of the work. This should be enough to flush everything except + // the middle which has lower priority. + ReactNoop.render(); + ReactNoop.flushLowPri(40); + + expect(ops).toEqual(['Foo', 'Bar', 'Bar']); + + ops = []; + + // Flush only the remaining work + ReactNoop.flush(); + + expect(ops).toEqual(['Middle']); });