diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 82e64b5357..390c4f6ac3 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -170,16 +170,12 @@ var ReactNoop = { ' '.repeat(depth) + '- ' + (fiber.type ? fiber.type.name || fiber.type : '[root]'), '[' + fiber.pendingWorkPriority + (fiber.pendingProps ? '*' : '') + ']' ); - const childInProgress = fiber.childInProgress; - if (childInProgress) { - if (childInProgress === fiber.child) { - console.log(' '.repeat(depth + 1) + 'ERROR: IN PROGRESS == CURRENT'); - } else { - console.log(' '.repeat(depth + 1) + 'IN PROGRESS'); - logFiber(childInProgress, depth + 1); - if (fiber.child) { - console.log(' '.repeat(depth + 1) + 'CURRENT'); - } + const childInProgress = fiber.progressedChild; + if (childInProgress && childInProgress !== fiber.child) { + console.log(' '.repeat(depth + 1) + 'IN PROGRESS: ' + fiber.progressedPriority); + logFiber(childInProgress, depth + 1); + if (fiber.child) { + console.log(' '.repeat(depth + 1) + 'CURRENT'); } } if (fiber.child) { diff --git a/src/renderers/shared/fiber/ReactChildFiber.js b/src/renderers/shared/fiber/ReactChildFiber.js index 90ea04d095..93839caed5 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -63,10 +63,10 @@ function ChildReconciler(shouldClone) { // Will fix reconciliation properly later. const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild; if (!shouldClone) { + // TODO: This might be lowering the priority of nested unfinished work. clone.pendingWorkPriority = priority; } clone.pendingProps = element.props; - clone.child = existingChild.child; clone.sibling = null; clone.return = returnFiber; previousSibling.sibling = clone; @@ -134,10 +134,10 @@ function ChildReconciler(shouldClone) { // Get the clone of the existing fiber. const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild; if (!shouldClone) { + // TODO: This might be lowering the priority of nested unfinished work. clone.pendingWorkPriority = priority; } clone.pendingProps = element.props; - clone.child = existingChild.child; clone.sibling = null; clone.return = returnFiber; return clone; @@ -219,3 +219,49 @@ function ChildReconciler(shouldClone) { exports.reconcileChildFibers = ChildReconciler(true); exports.reconcileChildFibersInPlace = ChildReconciler(false); + + +function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) { + workInProgress.return = returnFiber; + while (current.sibling) { + current = current.sibling; + workInProgress = workInProgress.sibling = cloneFiber( + current, + current.pendingWorkPriority + ); + workInProgress.return = returnFiber; + } + workInProgress.sibling = null; +} + +exports.cloneChildFibers = function(current : ?Fiber, workInProgress : Fiber) { + if (!workInProgress.child) { + return; + } + if (current && workInProgress.child === current.child) { + // We use workInProgress.child since that lets Flow know that it can't be + // null since we validated that already. However, as the line above suggests + // they're actually the same thing. + const currentChild = workInProgress.child; + // TODO: This used to reset the pending priority. Not sure if that is needed. + // workInProgress.pendingWorkPriority = current.pendingWorkPriority; + // TODO: The below priority used to be set to NoWork which would've + // dropped work. This is currently unobservable but will become + // observable when the first sibling has lower priority work remaining + // than the next sibling. At that point we should add tests that catches + // this. + const newChild = cloneFiber(currentChild, currentChild.pendingWorkPriority); + workInProgress.child = newChild; + cloneSiblings(currentChild, newChild, workInProgress); + } + + // If there is no alternate, then we don't need to clone the children. + // If the children of the alternate fiber is a different set, then we don't + // need to clone. We need to reset the return fiber though since we'll + // traverse down into them. + let child = workInProgress.child; + while (child) { + child.return = workInProgress; + child = child.sibling; + } +}; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index fb3d6f9723..967c6f78b0 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -89,25 +89,44 @@ export type Fiber = Instance & { firstEffect: ?Fiber, lastEffect: ?Fiber, - // This will be used to quickly determine if a subtree has no pending changes. pendingWorkPriority: PriorityLevel, + // This value represents the priority level that was last used to process this + // component. This indicates whether it is better to continue from the + // progressed work or if it is better to continue from the current state. + progressedPriority: PriorityLevel, + + // If work bails out on a Fiber that already had some work started at a lower + // priority, then we need to store the progressed work somewhere. This holds + // the started child set until we need to get back to working on it. It may + // or may not be the same as the "current" child. + progressedChild: ?Fiber, + // This is a pooled version of a Fiber. Every fiber that gets updated will // eventually have a pair. There are cases when we can clean up pairs to save // memory if we need to. alternate: ?Fiber, - // Keeps track of the children that are currently being processed but have not - // yet completed. - childInProgress: ?Fiber, - // Conceptual aliases // workInProgress : Fiber -> alternate The alternate used for reuse happens // to be the same as work in progress. }; +// This is a constructor of a POJO instead of a constructor function for a few +// reasons: +// 1) Nobody should add any instance methods on this. Instance methods can be +// more difficult to predict when they get optimized and they are almost +// never inlined properly in static compilers. +// 2) Nobody should rely on `instanceof Fiber` for type testing. We should +// always know when it is a fiber. +// 3) We can easily go from a createFiber call to calling a constructor if that +// is faster. The opposite is not true. +// 4) We might want to experiment with using numeric keys since they are easier +// to optimize in a non-JIT environment. +// 5) It should be easy to port this to a C struct and keep a C implementation +// compatible. var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { return { @@ -139,8 +158,8 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { lastEffect: null, pendingWorkPriority: NoWork, - - childInProgress: null, + progressedPriority: NoWork, + progressedChild: null, alternate: null, @@ -152,7 +171,16 @@ function shouldConstruct(Component) { } // This is used to create an alternate fiber to do work on. +// TODO: Rename to createWorkInProgressFiber or something like that. exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fiber { + // We clone to get a work in progress. That means that this fiber is the + // current. To make it safe to reuse that fiber later on as work in progress + // we need to reset its work in progress flag now. We don't have an + // opportunity to do this earlier since we don't traverse the tree when + // the work in progress tree becomes the current tree. + // fiber.progressedPriority = NoWork; + // fiber.progressedChild = null; + // We use a double buffering pooling technique because we know that we'll only // ever need at most two versions of a tree. We pool the "other" unused node // that we're free to reuse. This is lazily created to avoid allocating extra @@ -161,13 +189,15 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi let alt = fiber.alternate; if (alt) { alt.stateNode = fiber.stateNode; - alt.child = fiber.child; - alt.childInProgress = fiber.childInProgress; - alt.sibling = fiber.sibling; - alt.ref = alt.ref; - alt.pendingProps = fiber.pendingProps; + alt.sibling = fiber.sibling; // This should always be overridden. TODO: null + alt.ref = fiber.ref; + alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument. alt.pendingWorkPriority = priorityLevel; + alt.child = fiber.child; + alt.memoizedProps = fiber.memoizedProps; + alt.output = fiber.output; + // Whenever we clone, we do so to get a new work in progress. // This ensures that we've reset these in the new tree. alt.nextEffect = null; @@ -182,13 +212,19 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi alt.type = fiber.type; alt.stateNode = fiber.stateNode; alt.child = fiber.child; - alt.childInProgress = fiber.childInProgress; - alt.sibling = fiber.sibling; - alt.ref = alt.ref; + alt.sibling = fiber.sibling; // This should always be overridden. TODO: null + alt.ref = fiber.ref; // pendingProps is here for symmetry but is unnecessary in practice for now. + // TODO: Pass in the new pendingProps as an argument maybe? alt.pendingProps = fiber.pendingProps; alt.pendingWorkPriority = priorityLevel; + alt.memoizedProps = fiber.memoizedProps; + alt.output = fiber.output; + + alt.progressedChild = fiber.progressedChild; + alt.progressedPriority = fiber.progressedPriority; + alt.alternate = fiber; fiber.alternate = alt; return alt; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 6c664784f0..a21bd4059a 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -15,10 +15,12 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; import type { HostConfig } from 'ReactFiberReconciler'; +import type { PriorityLevel } from 'ReactPriorityLevel'; var { reconcileChildFibers, reconcileChildFibersInPlace, + cloneChildFibers, } = require('ReactChildFiber'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { @@ -35,48 +37,72 @@ var { NoWork, OffscreenPriority, } = require('ReactPriorityLevel'); -var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork'); module.exports = function(config : HostConfig) { + function markChildAsProgressed(current, workInProgress, priorityLevel) { + // We now have clones. Let's store them as the currently progressed work. + workInProgress.progressedChild = workInProgress.child; + workInProgress.progressedPriority = priorityLevel; + if (current) { + // We also store it on the current. When the alternate swaps in we can + // continue from this point. + current.progressedChild = workInProgress.progressedChild; + current.progressedPriority = workInProgress.progressedPriority; + } + } + function reconcileChildren(current, workInProgress, nextChildren) { - const priority = workInProgress.pendingWorkPriority; - reconcileChildrenAtPriority(current, workInProgress, nextChildren, priority); + const priorityLevel = workInProgress.pendingWorkPriority; + reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel); } function reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel) { - if (current && current.childInProgress) { - workInProgress.childInProgress = reconcileChildFibersInPlace( + // At this point any memoization is no longer valid since we'll have changed + // the children. + workInProgress.memoizedProps = null; + if (current && current.child === workInProgress.child) { + // If the current child is the same as the work in progress, it means that + // we haven't yet started any work on these children. Therefore, we use + // the clone algorithm to create a copy of all the current children. + workInProgress.child = reconcileChildFibers( workInProgress, - current.childInProgress, - nextChildren, - priorityLevel - ); - // This is now invalid because we reused nodes. - current.childInProgress = null; - } else if (workInProgress.childInProgress) { - workInProgress.childInProgress = reconcileChildFibersInPlace( - workInProgress, - workInProgress.childInProgress, + workInProgress.child, nextChildren, priorityLevel ); } else { - workInProgress.childInProgress = reconcileChildFibers( + // If, on the other hand, we don't have a current fiber or if it is + // already using a clone, that means we've already begun some work on this + // tree and we can continue where we left off by reconciling against the + // existing children. + workInProgress.child = reconcileChildFibersInPlace( workInProgress, - current ? current.child : null, + workInProgress.child, nextChildren, priorityLevel ); } + markChildAsProgressed(current, workInProgress, priorityLevel); } function updateFunctionalComponent(current, workInProgress) { var fn = workInProgress.type; var props = workInProgress.pendingProps; + + // TODO: Disable this before release, since it is not part of the public API + // I use this for testing to compare the relative overhead of classes. + if (typeof fn.shouldComponentUpdate === 'function') { + if (workInProgress.memoizedProps !== null) { + if (!fn.shouldComponentUpdate(workInProgress.memoizedProps, props)) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } + } + } + var nextChildren = fn(props); reconcileChildren(current, workInProgress, nextChildren); - workInProgress.pendingWorkPriority = NoWork; + return workInProgress.child; } function updateClassComponent(current : ?Fiber, workInProgress : Fiber) { @@ -86,14 +112,7 @@ module.exports = function(config : HostConfig) { var ctor = workInProgress.type; workInProgress.stateNode = instance = new ctor(props); } else if (typeof instance.shouldComponentUpdate === 'function') { - if (current && current.memoizedProps) { - // Revert to the last flushed props, incase we aborted an update. - instance.props = current.memoizedProps; - if (!instance.shouldComponentUpdate(props)) { - return bailoutOnCurrent(current, workInProgress); - } - } - if (!workInProgress.childInProgress && workInProgress.memoizedProps) { + if (workInProgress.memoizedProps !== null) { // Reset the props, in case this is a ping-pong case rather than a // completed update case. For the completed update case, the instance // props will already be the memoizedProps. @@ -103,34 +122,40 @@ module.exports = function(config : HostConfig) { } } } + instance.props = props; var nextChildren = instance.render(); reconcileChildren(current, workInProgress, nextChildren); - workInProgress.pendingWorkPriority = NoWork; - return workInProgress.childInProgress; + + return workInProgress.child; } function updateHostComponent(current, workInProgress) { - var nextChildren = workInProgress.pendingProps.children; + const nextChildren = workInProgress.pendingProps.children; + if (workInProgress.pendingProps.hidden && + workInProgress.pendingWorkPriority !== OffscreenPriority) { + // If this host component is hidden, we can bail out on the children. + // We'll rerender the children later at the lower priority. - let priority = workInProgress.pendingWorkPriority; - if (workInProgress.pendingProps.hidden && priority !== OffscreenPriority) { - // If this host component is hidden, we can reconcile its children at - // the lowest priority and bail out from this particular pass. Unless, we're - // currently reconciling the lowest priority. - // If we have a child in progress already, we reconcile against that set - // to retain any work within it. We'll recreate any component that was in - // the current set and next set but not in the previous in progress set. - // TODO: This attaches a node that hasn't completed rendering so it - // becomes part of the render tree, even though it never completed. Its - // `output` property is unpredictable because of it. + // It is unfortunate that we have to do the reconciliation of these + // children already since that will add them to the tree even though + // they are not actually done yet. If this is a large set it is also + // confusing that this takes time to do right now instead of later. + + if (workInProgress.progressedPriority === OffscreenPriority) { + // If we already made some progress on the offscreen priority before, + // then we should continue from where we left off. + workInProgress.child = workInProgress.progressedChild; + } + + // Reconcile the children and stash them for later work. reconcileChildrenAtPriority(current, workInProgress, nextChildren, OffscreenPriority); - workInProgress.pendingWorkPriority = OffscreenPriority; + workInProgress.child = current ? current.child : null; + // Abort and don't process children yet. return null; } else { reconcileChildren(current, workInProgress, nextChildren); - workInProgress.pendingWorkPriority = NoWork; - return workInProgress.childInProgress; + return workInProgress.child; } } @@ -141,19 +166,19 @@ module.exports = function(config : HostConfig) { if (typeof value === 'object' && value && typeof value.render === 'function') { // Proceed under the assumption that this is a class instance workInProgress.tag = ClassComponent; - if (workInProgress.alternate) { - workInProgress.alternate.tag = ClassComponent; + if (current) { + current.tag = ClassComponent; } value = value.render(); } else { // Proceed under the assumption that this is a functional component workInProgress.tag = FunctionalComponent; - if (workInProgress.alternate) { - workInProgress.alternate.tag = FunctionalComponent; + if (current) { + current.tag = FunctionalComponent; } } reconcileChildren(current, workInProgress, value); - workInProgress.pendingWorkPriority = NoWork; + return workInProgress.child; } function updateCoroutineComponent(current, workInProgress) { @@ -162,29 +187,9 @@ module.exports = function(config : HostConfig) { throw new Error('Should be resolved by now'); } reconcileChildren(current, workInProgress, coroutine.children); - workInProgress.pendingWorkPriority = NoWork; - } - - function reuseChildren(returnFiber : Fiber, firstChild : Fiber) { - // TODO: None of this should be necessary if structured better. - // The returnFiber pointer only needs to be updated when we walk into this child - // which we don't do right now. If the pending work priority indicated only - // if a child has work rather than if the node has work, then we would know - // by a single lookup on workInProgress rather than having to go through - // each child. - let child = firstChild; - do { - // Update the returnFiber of the child to the newest fiber. - child.return = returnFiber; - // Retain the priority if there's any work left to do in the children. - if (child.pendingWorkPriority !== NoWork && - (returnFiber.pendingWorkPriority === NoWork || - returnFiber.pendingWorkPriority > child.pendingWorkPriority)) { - returnFiber.pendingWorkPriority = child.pendingWorkPriority; - } - } while (child = child.sibling); } + /* function reuseChildrenEffects(returnFiber : Fiber, firstChild : Fiber) { let child = firstChild; do { @@ -201,113 +206,81 @@ module.exports = function(config : HostConfig) { } } while (child = child.sibling); } - - function bailoutOnCurrent(current : Fiber, workInProgress : Fiber) : ?Fiber { - // The most likely scenario is that the previous copy of the tree contains - // the same props as the new one. In that case, we can just copy the output - // and children from that node. - workInProgress.memoizedProps = workInProgress.pendingProps; - workInProgress.output = current.output; - const priorityLevel = workInProgress.pendingWorkPriority; - workInProgress.pendingProps = null; - workInProgress.pendingWorkPriority = NoWork; - workInProgress.stateNode = current.stateNode; - workInProgress.childInProgress = current.childInProgress; - if (current.child) { - // If we bail out but still has work with the current priority in this - // subtree, we need to go find it right now. If we don't, we won't flush - // it until the next tick. - workInProgress.child = current.child; - reuseChildren(workInProgress, workInProgress.child); - if (workInProgress.pendingWorkPriority !== NoWork && workInProgress.pendingWorkPriority <= priorityLevel) { - // TODO: This passes the current node and reads the priority level and - // pending props from that. We want it to read our priority level and - // pending props from the work in progress. Needs restructuring. - return findNextUnitOfWorkAtPriority(current, priorityLevel); - } else { - return null; - } - } else { - workInProgress.child = null; - return null; - } - } + */ function bailoutOnAlreadyFinishedWork(current, workInProgress : Fiber) : ?Fiber { - // If we started this work before, and finished it, or if we're in a - // ping-pong update scenario, this version could already be what we're - // looking for. In that case, we should be able to just bail out. const priorityLevel = workInProgress.pendingWorkPriority; - workInProgress.pendingProps = null; - workInProgress.pendingWorkPriority = NoWork; - workInProgress.firstEffect = null; - workInProgress.nextEffect = null; - workInProgress.lastEffect = null; + // TODO: We should ideally be able to bail out early if the children have no + // more work to do. However, since we don't have a separation of this + // Fiber's priority and its children yet - we don't know without doing lots + // of the same work we do anyway. Once we have that separation we can just + // bail out here if the children has no more work at this priority level. + // if (workInProgress.priorityOfChildren <= priorityLevel) { + // // If there are side-effects in these children that have not yet been + // // committed we need to ensure that they get properly transferred up. + // if (current && current.child !== workInProgress.child) { + // reuseChildrenEffects(workInProgress, child); + // } + // return null; + // } - if (workInProgress.child) { - // On the way up here, we reset the child node to be the current one by - // cloning. However, it is really the original child that represents the - // already completed work. Therefore we have to reuse the alternate. - // But if we don't have a current, this was not cloned. This is super weird. - const child = !current ? workInProgress.child : workInProgress.child.alternate; - if (!child) { - throw new Error('We must have a current child to be able to use this.'); - } - workInProgress.child = child; - // Ensure that the effects of reused work are preserved. - reuseChildrenEffects(workInProgress, child); - // If we bail out but still has work with the current priority in this - // subtree, we need to go find it right now. If we don't, we won't flush - // it until the next tick. - reuseChildren(workInProgress, child); - if (workInProgress.pendingWorkPriority !== NoWork && - workInProgress.pendingWorkPriority <= priorityLevel) { - // TODO: This passes the current node and reads the priority level and - // pending props from that. We want it to read our priority level and - // pending props from the work in progress. Needs restructuring. - return findNextUnitOfWorkAtPriority(workInProgress, priorityLevel); - } + cloneChildFibers(current, workInProgress); + markChildAsProgressed(current, workInProgress, priorityLevel); + return workInProgress.child; + } + + function bailoutOnLowPriority(current, workInProgress) { + if (current) { + workInProgress.child = current.child; + workInProgress.memoizedProps = current.memoizedProps; + workInProgress.output = current.output; } return null; } - function beginWork(current : ?Fiber, workInProgress : Fiber) : ?Fiber { - // 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 - // progress. - if (current && workInProgress.pendingProps === current.memoizedProps) { - return bailoutOnCurrent(current, workInProgress); + function beginWork(current : ?Fiber, workInProgress : Fiber, priorityLevel : PriorityLevel) : ?Fiber { + if (workInProgress.pendingWorkPriority === NoWork || + workInProgress.pendingWorkPriority > priorityLevel) { + return bailoutOnLowPriority(current, workInProgress); } - if (!workInProgress.childInProgress && - workInProgress.pendingProps === workInProgress.memoizedProps) { + if (workInProgress.progressedPriority === priorityLevel) { + // If we have progressed work on this priority level already, we can + // proceed this that as the child. + workInProgress.child = workInProgress.progressedChild; + } + + if (workInProgress.pendingProps === null || ( + workInProgress.memoizedProps !== null && + workInProgress.pendingProps === workInProgress.memoizedProps + )) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } switch (workInProgress.tag) { case IndeterminateComponent: - mountIndeterminateComponent(current, workInProgress); - return workInProgress.childInProgress; + return mountIndeterminateComponent(current, workInProgress); case FunctionalComponent: - updateFunctionalComponent(current, workInProgress); - return workInProgress.childInProgress; + return updateFunctionalComponent(current, workInProgress); case ClassComponent: return updateClassComponent(current, workInProgress); case HostContainer: 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.childInProgress) { + if (workInProgress.child) { return beginWork( - workInProgress.childInProgress.alternate, - workInProgress.childInProgress + workInProgress.child.alternate, + workInProgress.child, + priorityLevel ); } return null; case HostComponent: + if (workInProgress.stateNode && config.beginUpdate) { + config.beginUpdate(workInProgress.stateNode); + } return updateHostComponent(current, workInProgress); case CoroutineHandlerPhase: // This is a restart. Reset the tag to the initial phase. @@ -317,21 +290,22 @@ module.exports = function(config : HostConfig) { updateCoroutineComponent(current, workInProgress); // This doesn't take arbitrary time so we could synchronously just begin // eagerly do the work of workInProgress.child as an optimization. - if (workInProgress.childInProgress) { + if (workInProgress.child) { return beginWork( - workInProgress.childInProgress.alternate, - workInProgress.childInProgress + workInProgress.child.alternate, + workInProgress.child, + priorityLevel ); } - return workInProgress.childInProgress; + return workInProgress.child; 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, - workInProgress.sibling + workInProgress.sibling, + priorityLevel ); } return null; diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index c7a9f2f710..2e3e8ae3d0 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -28,7 +28,7 @@ module.exports = function(config : HostConfig) { const updateContainer = config.updateContainer; const commitUpdate = config.commitUpdate; - function commitWork(finishedWork : Fiber) : void { + function commitWork(current : ?Fiber, finishedWork : Fiber) : void { switch (finishedWork.tag) { case ClassComponent: { // TODO: Fire componentDidMount/componentDidUpdate, update refs @@ -43,14 +43,13 @@ module.exports = function(config : HostConfig) { return; } case HostComponent: { - if (finishedWork.stateNode == null || !finishedWork.alternate) { + if (finishedWork.stateNode == null || !current) { throw new Error('This should only be done during updates.'); } // Commit the work prepared earlier. const child = finishedWork.child; const children = (child && !child.sibling) ? (child.output : ?Fiber | I) : child; const newProps = finishedWork.memoizedProps; - const current = finishedWork.alternate; const oldProps = current.memoizedProps; const instance : I = finishedWork.stateNode; commitUpdate(instance, oldProps, newProps, children); diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 53421116b4..14e95a0d73 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -162,10 +162,16 @@ module.exports = function(config : HostConfig) { // This returns true if there was something to update. markForPreEffect(workInProgress); } + // TODO: Is this actually ever going to change? Why set it every time? workInProgress.output = instance; } else { if (!newProps) { - throw new Error('We must have new props for new mounts.'); + if (workInProgress.stateNode === null) { + throw new Error('We must have new props for new mounts.'); + } else { + // This can happen when we abort work. + return null; + } } const instance = createInstance(workInProgress.type, newProps, children); // TODO: This seems like unnecessary duplication. diff --git a/src/renderers/shared/fiber/ReactFiberPendingWork.js b/src/renderers/shared/fiber/ReactFiberPendingWork.js deleted file mode 100644 index eb61c4595f..0000000000 --- a/src/renderers/shared/fiber/ReactFiberPendingWork.js +++ /dev/null @@ -1,126 +0,0 @@ -/** - * Copyright 2013-present, Facebook, Inc. - * All rights reserved. - * - * This source code is licensed under the BSD-style license found in the - * LICENSE file in the root directory of this source tree. An additional grant - * of patent rights can be found in the PATENTS file in the same directory. - * - * @providesModule ReactFiberPendingWork - * @flow - */ - -'use strict'; - -import type { Fiber } from 'ReactFiber'; -import type { PriorityLevel } from 'ReactPriorityLevel'; - -var { cloneFiber } = require('ReactFiber'); - -var { - NoWork, -} = require('ReactPriorityLevel'); - -function cloneSiblings(current : Fiber, workInProgress : Fiber, returnFiber : Fiber) { - workInProgress.return = returnFiber; - while (current.sibling) { - current = current.sibling; - workInProgress = workInProgress.sibling = cloneFiber( - current, - current.pendingWorkPriority - ); - workInProgress.return = returnFiber; - } - workInProgress.sibling = null; -} - -exports.findNextUnitOfWorkAtPriority = function(currentRoot : Fiber, priorityLevel : PriorityLevel) : ?Fiber { - let current = currentRoot; - while (current) { - if (current.pendingWorkPriority !== NoWork && - current.pendingWorkPriority <= priorityLevel) { - // This node has work to do that fits our priority level criteria. - 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. - const workInProgress = current.alternate; - if (!workInProgress) { - throw new Error('Should have wip now'); - } - workInProgress.pendingProps = current.pendingProps; - return workInProgress; - } - - // 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.childInProgress) { - let workInProgress = current.childInProgress; - while (workInProgress) { - workInProgress.return = current.alternate; - workInProgress = workInProgress.sibling; - } - workInProgress = current.childInProgress; - while (workInProgress) { - // Don't bother drilling further down this tree if there is no child. - if (workInProgress.pendingWorkPriority !== NoWork && - workInProgress.pendingWorkPriority <= priorityLevel && - workInProgress.pendingProps !== null) { - return workInProgress; - } - workInProgress = workInProgress.sibling; - } - } else if (current.child) { - let currentChild = current.child; - currentChild.return = current; - // Ensure we have a work in progress copy to backtrack through. - let workInProgress = current.alternate; - if (!workInProgress) { - throw new Error('Should have wip now'); - } - workInProgress.pendingWorkPriority = current.pendingWorkPriority; - // TODO: The below priority used to be set to NoWork which would've - // dropped work. This is currently unobservable but will become - // observable when the first sibling has lower priority work remaining - // than the next sibling. At that point we should add tests that catches - // this. - workInProgress.child = cloneFiber( - currentChild, - currentChild.pendingWorkPriority - ); - cloneSiblings(currentChild, workInProgress.child, workInProgress); - current = currentChild; - continue; - } - // If we match the priority but has no child and no work to do, - // then we can safely reset the flag. - current.pendingWorkPriority = NoWork; - } - if (current === currentRoot) { - if (current.pendingWorkPriority <= priorityLevel) { - // If this subtree had work left to do, we would have returned it by - // now. This could happen if a child with pending work gets cleaned up - // but we don't clear the flag then. It is safe to reset it now. - current.pendingWorkPriority = NoWork; - } - return null; - } - while (!current.sibling) { - current = current.return; - if (!current) { - return null; - } - if (current.pendingWorkPriority <= priorityLevel) { - // If this subtree had work left to do, we would have returned it by - // now. This could happen if a child with pending work gets cleaned up - // but we don't clear the flag then. It is safe to reset it now. - current.pendingWorkPriority = NoWork; - } - } - current.sibling.return = current.return; - current = current.sibling; - } - return null; -}; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index d5e03669e6..bad5ee9477 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -70,6 +70,8 @@ module.exports = function(config : HostConfig) : Reconci const root = createFiberRoot(containerInfo); const container = root.current; // TODO: Use pending work/state instead of props. + // TODO: This should not override the pendingWorkPriority if there is + // higher priority work in the subtree. container.pendingProps = element; container.pendingWorkPriority = LowPriority; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index b6e7128968..e04c846f20 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -22,13 +22,9 @@ var ReactFiberCompleteWork = require('ReactFiberCompleteWork'); var ReactFiberCommitWork = require('ReactFiberCommitWork'); var { cloneFiber } = require('ReactFiber'); -var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork'); var { NoWork, - HighPriority, - LowPriority, - OffscreenPriority, } = require('ReactPriorityLevel'); var timeHeuristicForUnitOfWork = 1; @@ -65,33 +61,23 @@ module.exports = function(config : HostConfig) { // TODO: This is scanning one root at a time. It should be scanning all // roots for high priority work before moving on to lower priorities. let root = nextScheduledRoot; + let highestPriorityRoot = null; + let highestPriorityLevel = NoWork; while (root) { - cloneFiber(root.current, root.current.pendingWorkPriority); - // 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(root.current, HighPriority); - if (work) { - nextPriorityLevel = HighPriority; - return work; - } - work = findNextUnitOfWorkAtPriority(root.current, LowPriority); - if (work) { - nextPriorityLevel = LowPriority; - return work; - } - work = findNextUnitOfWorkAtPriority(root.current, OffscreenPriority); - if (work) { - nextPriorityLevel = OffscreenPriority; - return work; + if (highestPriorityLevel === NoWork || + highestPriorityLevel > root.current.pendingWorkPriority) { + highestPriorityLevel = root.current.pendingWorkPriority; + highestPriorityRoot = root; } // We didn't find anything to do in this root, so let's try the next one. root = root.nextScheduledRoot; } - root = nextScheduledRoot; - while (root) { - root = root.nextScheduledRoot; + if (highestPriorityRoot) { + nextPriorityLevel = highestPriorityLevel; + return cloneFiber( + highestPriorityRoot.current, + highestPriorityLevel + ); } nextPriorityLevel = NoWork; @@ -103,7 +89,8 @@ module.exports = function(config : HostConfig) { // TODO: Error handling. let effectfulFiber = finishedWork.firstEffect; while (effectfulFiber) { - commitWork(effectfulFiber); + const current = effectfulFiber.alternate; + commitWork(current, effectfulFiber); const next = effectfulFiber.nextEffect; // Ensure that we clean these up so that we don't accidentally keep them. // I'm not actually sure this matters because we can't reset firstEffect @@ -114,6 +101,24 @@ module.exports = function(config : HostConfig) { } } + function resetWorkPriority(workInProgress : Fiber) { + let newPriority = NoWork; + // progressedChild is going to be the child set with the highest priority. + // Either it is the same as child, or it just bailed out because it choose + // not to do the work. + let child = workInProgress.progressedChild; + while (child) { + // Ensure that remaining work priority bubbles up. + if (child.pendingWorkPriority !== NoWork && + (newPriority === NoWork || + newPriority > child.pendingWorkPriority)) { + newPriority = child.pendingWorkPriority; + } + child = child.sibling; + } + workInProgress.pendingWorkPriority = newPriority; + } + function completeUnitOfWork(workInProgress : Fiber) : ?Fiber { while (true) { // The current, flushed, state of this fiber is the alternate. @@ -123,6 +128,8 @@ module.exports = function(config : HostConfig) { const current = workInProgress.alternate; const next = completeWork(current, workInProgress); + resetWorkPriority(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; @@ -130,12 +137,6 @@ module.exports = function(config : HostConfig) { const returnFiber = workInProgress.return; if (returnFiber) { - // Ensure that remaining work priority bubbles up. - if (workInProgress.pendingWorkPriority !== NoWork && - (returnFiber.pendingWorkPriority === NoWork || - returnFiber.pendingWorkPriority > workInProgress.pendingWorkPriority)) { - returnFiber.pendingWorkPriority = workInProgress.pendingWorkPriority; - } // Ensure that the first and last effect of the parent corresponds // to the children's first and last effect. This probably relies on // children completing in order. @@ -159,18 +160,16 @@ module.exports = function(config : HostConfig) { } else if (returnFiber) { // If there's no more work in this returnFiber. Complete the returnFiber. workInProgress = returnFiber; - // If we're stepping up through the child, that means we can now commit - // this work. We should only do this when we're stepping upwards because - // completing a downprioritized item is not the same as completing its - // children. - if (workInProgress.childInProgress) { - workInProgress.child = workInProgress.childInProgress; - workInProgress.childInProgress = null; - } continue; } else { // If we're at the root, there's no more work to do. We can flush it. const root : FiberRoot = (workInProgress.stateNode : any); + if (root.current === workInProgress) { + throw new Error( + 'Cannot commit the same tree as before. This is probably a bug ' + + 'related to the return field.' + ); + } root.current = workInProgress; // TODO: We can be smarter here and only look for more work in the // "next" scheduled work since we've already scanned passed. That @@ -191,16 +190,13 @@ module.exports = function(config : HostConfig) { } function performUnitOfWork(workInProgress : Fiber) : ?Fiber { - // Ignore work if there is nothing to do. - if (workInProgress.pendingProps === null) { - return completeUnitOfWork(workInProgress); - } // 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 // progress. const current = workInProgress.alternate; - const next = beginWork(current, workInProgress); + const next = beginWork(current, workInProgress, nextPriorityLevel); + if (next) { // If this spawns new work, do that next. return next; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index f69f84691b..608a95ea4f 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -410,7 +410,7 @@ describe('ReactIncremental', () => { // Init ReactNoop.render(); - ReactNoop.flushLowPri(55); + ReactNoop.flushLowPri(55 + 25); // We only finish the higher priority work. So the low pri content // has not yet finished mounting. @@ -432,7 +432,7 @@ describe('ReactIncremental', () => { // Make a quick update which will schedule low priority work to // update the middle content. ReactNoop.render(); - ReactNoop.flushLowPri(30); + ReactNoop.flushLowPri(30 + 25); expect(ops).toEqual(['Foo', 'Bar']); @@ -526,7 +526,7 @@ describe('ReactIncremental', () => { ops = []; // The middle content is now pending rendering... - ReactNoop.flushLowPri(30); + ReactNoop.flushLowPri(30 + 25); expect(ops).toEqual(['Content', 'Middle', 'Bar']); // One more Middle left. ops = []; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index fbf6d0a712..63983720df 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -177,7 +177,7 @@ describe('ReactIncrementalSideEffects', () => { // render some higher priority work. The middle content will bailout so // it remains untouched which means that it should reuse it next time. ReactNoop.render(); - ReactNoop.flush(30); + ReactNoop.flush(); // Since we did nothing to the middle subtree during the interuption, // we should be able to reuse the reconciliation work that we already did @@ -270,6 +270,86 @@ describe('ReactIncrementalSideEffects', () => { ]); }); + it('can defer side-effects and resume them later on', function() { + class Bar extends React.Component { + shouldComponentUpdate(nextProps) { + return this.props.idx !== nextProps; + } + render() { + return ; + } + } + function Foo(props) { + return ( +
+ + +
+ ); + } + ReactNoop.render(); + ReactNoop.flushLowPri(40 + 25); + expect(ReactNoop.root.children).toEqual([ + div( + span(0), + div(/*the spans are down-prioritized and not rendered yet*/) + ), + ]); + ReactNoop.render(); + ReactNoop.flushLowPri(35 + 25); + expect(ReactNoop.root.children).toEqual([ + div( + span(1), + div(/*still not rendered yet*/) + ), + ]); + ReactNoop.flushLowPri(30 + 25); + expect(ReactNoop.root.children).toEqual([ + div( + span(1), + div( + // Now we had enough time to finish the spans. + span(0), + span(1) + ) + ), + ]); + var innerSpanA = ReactNoop.root.children[0].children[1].children[1]; + ReactNoop.render(); + ReactNoop.flushLowPri(30 + 25); + expect(ReactNoop.root.children).toEqual([ + div( + span(2), + div( + // Still same old numbers. + span(0), + span(1) + ) + ), + ]); + ReactNoop.flushLowPri(30); + expect(ReactNoop.root.children).toEqual([ + div( + span(2), + div( + // New numbers. + span(1), + span(2) + ) + ), + ]); + + var innerSpanB = ReactNoop.root.children[0].children[1].children[1]; + // This should have been an update to an existing instance, not recreation. + // We verify that by ensuring that the child instance was the same as + // before. + expect(innerSpanA).toBe(innerSpanB); + }); + + // TODO: Test that side-effects are not cut off when a work in progress node // moves to "current" without flushing due to having lower priority. Does this // even happen? Maybe a child doesn't get processed because it is lower prio?