diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index 82e64b53579..390c4f6ac38 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 90ea04d0951..26ee1dac2fb 100644 --- a/src/renderers/shared/fiber/ReactChildFiber.js +++ b/src/renderers/shared/fiber/ReactChildFiber.js @@ -26,6 +26,7 @@ var { var ReactFiber = require('ReactFiber'); var ReactReifiedYield = require('ReactReifiedYield'); +var ReactPriorityLevel = require('ReactPriorityLevel'); const { cloneFiber, @@ -38,6 +39,10 @@ const { createReifiedYield, } = ReactReifiedYield; +const { + NoWork, +} = ReactPriorityLevel; + const isArray = Array.isArray; function ChildReconciler(shouldClone) { @@ -63,10 +68,15 @@ function ChildReconciler(shouldClone) { // Will fix reconciliation properly later. const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild; if (!shouldClone) { - clone.pendingWorkPriority = priority; + // TODO: This might be lowering the priority of nested unfinished work. + clone.pendingUpdatePriority = priority; + if (clone.pendingWorkPriority === NoWork || + clone.pendingWorkPriority > priority) { + clone.pendingWorkPriority = priority; + } } clone.pendingProps = element.props; - clone.child = existingChild.child; + // clone.child = existingChild.child; clone.sibling = null; clone.return = returnFiber; previousSibling.sibling = clone; @@ -134,10 +144,15 @@ function ChildReconciler(shouldClone) { // Get the clone of the existing fiber. const clone = shouldClone ? cloneFiber(existingChild, priority) : existingChild; if (!shouldClone) { - clone.pendingWorkPriority = priority; + // TODO: This might be lowering the priority of nested unfinished work. + clone.pendingUpdatePriority = priority; + if (clone.pendingWorkPriority === NoWork || + clone.pendingWorkPriority > priority) { + clone.pendingWorkPriority = priority; + } } clone.pendingProps = element.props; - clone.child = existingChild.child; + // clone.child = existingChild.child; clone.sibling = null; clone.return = returnFiber; return clone; @@ -219,3 +234,54 @@ 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(workInProgress : Fiber) { + if (!workInProgress.child) { + return; + } + const current = workInProgress.alternate; + if (!current || workInProgress.child !== current.child) { + // 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; + } + return; + } + // 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 currentChild = workInProgress.child; + if (!currentChild) { + return; + } + workInProgress.child = cloneFiber( + currentChild, + currentChild.pendingWorkPriority + ); + cloneSiblings(currentChild, workInProgress.child, workInProgress); +}; diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index fb3d6f9723f..0e083345069 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -15,6 +15,7 @@ import type { ReactCoroutine, ReactYield } from 'ReactCoroutine'; import type { TypeOfWork } from 'ReactTypeOfWork'; import type { PriorityLevel } from 'ReactPriorityLevel'; +import type { UpdateQueue } from 'ReactFiberUpdateQueue'; var ReactTypeOfWork = require('ReactTypeOfWork'); var { @@ -76,6 +77,12 @@ export type Fiber = Instance & { pendingProps: any, // This type will be more specific once we overload the tag. // TODO: I think that there is a way to merge pendingProps and memoizedProps. memoizedProps: any, // The props used to create the output. + // A queue of local state updates. + updateQueue: ?UpdateQueue, + // The state used to create the output. This is a full state object. + memoizedState: any, + // Linked list of callbacks to call after updates are committed. + callbackList: ?UpdateQueue, // Output is the return value of this fiber, or a linked list of return values // if this returns multiple values. Such as a fragment. output: any, // This type will be more specific once we overload the tag. @@ -89,25 +96,49 @@ export type Fiber = Instance & { firstEffect: ?Fiber, lastEffect: ?Fiber, + // The update priority is the priority of a fiber's pending props and state. + // It may be lower than the priority of the entire subtree. + pendingUpdatePriority: PriorityLevel, - // This will be used to quickly determine if a subtree has no pending changes. + // The work priority is the priority of the entire subtree. It 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 { @@ -132,15 +163,19 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { pendingProps: null, memoizedProps: null, + updateQueue: null, + memoizedState: null, + callbackList: null, output: null, nextEffect: null, firstEffect: null, lastEffect: null, + pendingUpdatePriority: NoWork, pendingWorkPriority: NoWork, - - childInProgress: null, + progressedPriority: NoWork, + progressedChild: null, alternate: null, @@ -152,7 +187,38 @@ 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; + + // Don't deprioritize when cloning. Unlike other priority comparisons (e.g. + // in the scheduler), this one must check that priorityLevel is not equal to + // NoWork, otherwise work will be dropped. For complete correctness, the other + // priority comparisons should also perform this check, even though it's not + // an issue in practice. I didn't catch this at first and it created a subtle + // bug, which suggests we may need to extract the logic into a + // utility function (shouldOverridePriority). + let updatePriority; + let workPriority; + if (priorityLevel !== NoWork && + (priorityLevel < fiber.pendingUpdatePriority || fiber.pendingUpdatePriority === NoWork)) { + updatePriority = priorityLevel; + } else { + updatePriority = fiber.pendingUpdatePriority; + } + if (updatePriority !== NoWork && + (updatePriority < fiber.pendingWorkPriority || fiber.pendingWorkPriority === NoWork)) { + workPriority = updatePriority; + } else { + workPriority = fiber.pendingWorkPriority; + } + // 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,12 +227,17 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi let alt = fiber.alternate; if (alt) { alt.stateNode = fiber.stateNode; + alt.sibling = fiber.sibling; // This should always be overridden. TODO: null + alt.ref = fiber.ref; + alt.pendingProps = fiber.pendingProps; // TODO: Pass as argument. + alt.updateQueue = fiber.updateQueue; + alt.callbackList = fiber.callbackList; + alt.pendingUpdatePriority = updatePriority; + alt.pendingWorkPriority = workPriority; + alt.child = fiber.child; - alt.childInProgress = fiber.childInProgress; - alt.sibling = fiber.sibling; - alt.ref = alt.ref; - alt.pendingProps = fiber.pendingProps; - alt.pendingWorkPriority = priorityLevel; + 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. @@ -182,12 +253,21 @@ 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.updateQueue = fiber.updateQueue; + alt.callbackList = fiber.callbackList; + alt.pendingUpdatePriority = updatePriority; + alt.pendingWorkPriority = workPriority; + + alt.memoizedProps = fiber.memoizedProps; + alt.output = fiber.output; + + alt.progressedChild = fiber.progressedChild; + alt.progressedPriority = fiber.progressedPriority; alt.alternate = fiber; fiber.alternate = alt; @@ -203,6 +283,7 @@ exports.createFiberFromElement = function(element : ReactElement<*>, priorityLev // $FlowFixMe: ReactElement.key is currently defined as ?string but should be defined as null | string in Flow. const fiber = createFiberFromElementType(element.type, element.key); fiber.pendingProps = element.props; + fiber.pendingUpdatePriority = priorityLevel; fiber.pendingWorkPriority = priorityLevel; return fiber; }; @@ -232,6 +313,7 @@ exports.createFiberFromCoroutine = function(coroutine : ReactCoroutine, priority const fiber = createFiber(CoroutineComponent, coroutine.key); fiber.type = coroutine.handler; fiber.pendingProps = coroutine; + fiber.pendingUpdatePriority = priorityLevel; fiber.pendingWorkPriority = priorityLevel; return fiber; }; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index 6c664784f07..03b73f81e71 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -14,12 +14,18 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; +import type { FiberRoot } from 'ReactFiberRoot'; import type { HostConfig } from 'ReactFiberReconciler'; +import type { Scheduler } from 'ReactFiberScheduler'; +import type { PriorityLevel } from 'ReactPriorityLevel'; +import type { UpdateQueue } from 'ReactFiberUpdateQueue'; var { reconcileChildFibers, reconcileChildFibersInPlace, + cloneChildFibers, } = require('ReactChildFiber'); +var { LowPriority } = require('ReactPriorityLevel'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { IndeterminateComponent, @@ -35,102 +41,238 @@ var { NoWork, OffscreenPriority, } = require('ReactPriorityLevel'); -var { findNextUnitOfWorkAtPriority } = require('ReactFiberPendingWork'); +var { + createUpdateQueue, + addToQueue, + addCallbackToQueue, + mergeUpdateQueue, +} = require('ReactFiberUpdateQueue'); +var ReactInstanceMap = require('ReactInstanceMap'); + +module.exports = function(config : HostConfig, getScheduler : () => Scheduler) { -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.pendingUpdatePriority; + reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel); } function reconcileChildrenAtPriority(current, workInProgress, nextChildren, priorityLevel) { - if (current && current.childInProgress) { - workInProgress.childInProgress = reconcileChildFibersInPlace( - workInProgress, - current.childInProgress, - nextChildren, - priorityLevel - ); - // This is now invalid because we reused nodes. - current.childInProgress = null; - } else if (workInProgress.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, - 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 scheduleUpdate(fiber: Fiber, updateQueue: UpdateQueue, priorityLevel : PriorityLevel): void { + const { scheduleLowPriWork } = getScheduler(); + fiber.updateQueue = updateQueue; + // Schedule update on the alternate as well, since we don't know which tree + // is current. + if (fiber.alternate) { + fiber.alternate.updateQueue = updateQueue; + } + + // Set the update priority of the fiber and its alternate + if (fiber.pendingUpdatePriority === NoWork || + fiber.pendingUpdatePriority > priorityLevel) { + fiber.pendingUpdatePriority = priorityLevel; + } + if (fiber.alternate) { + if (fiber.alternate.pendingUpdatePriority === NoWork || + fiber.alternate.pendingUpdatePriority > priorityLevel) { + fiber.alternate.pendingUpdatePriority = priorityLevel; + } + } + + // For this fiber and all its ancestors and their alternates, set the + // work (subtree) priority + while (true) { + if (fiber.pendingWorkPriority === NoWork || + fiber.pendingWorkPriority > priorityLevel) { + fiber.pendingWorkPriority = priorityLevel; + } + if (fiber.alternate) { + if (fiber.alternate.pendingWorkPriority === NoWork || + fiber.alternate.pendingWorkPriority > priorityLevel) { + fiber.alternate.pendingWorkPriority = priorityLevel; + } + } + // Duck type root + if (fiber.stateNode && fiber.stateNode.containerInfo) { + const root : FiberRoot = (fiber.stateNode : any); + scheduleLowPriWork(root, priorityLevel); + return; + } + if (!fiber.return) { + throw new Error('No root!'); + } + fiber = fiber.return; + } + } + + // Class component state updater + const updater = { + enqueueSetState(instance, partialState) { + const fiber = ReactInstanceMap.get(instance); + const updateQueue = fiber.updateQueue ? + addToQueue(fiber.updateQueue, partialState) : + createUpdateQueue(partialState); + scheduleUpdate(fiber, updateQueue, LowPriority); + }, + enqueueReplaceState(instance, state) { + const fiber = ReactInstanceMap.get(instance); + const updateQueue = createUpdateQueue(state); + updateQueue.isReplace = true; + scheduleUpdate(fiber, updateQueue, LowPriority); + }, + enqueueForceUpdate(instance) { + const fiber = ReactInstanceMap.get(instance); + const updateQueue = fiber.updateQueue || createUpdateQueue(null); + updateQueue.isForced = true; + scheduleUpdate(fiber, updateQueue, LowPriority); + }, + enqueueCallback(instance, callback) { + const fiber = ReactInstanceMap.get(instance); + let updateQueue = fiber.updateQueue ? + fiber.updateQueue : + createUpdateQueue(null); + addCallbackToQueue(updateQueue, callback); + fiber.updateQueue = updateQueue; + if (fiber.alternate) { + fiber.alternate.updateQueue = updateQueue; + } + }, + }; + function updateClassComponent(current : ?Fiber, workInProgress : Fiber) { + // A class component update is the result of either new props or new state. + // Account for the possibly of missing pending props by falling back to the + // memoized props. var props = workInProgress.pendingProps; + if (!props && current) { + props = current.memoizedProps; + } + // Compute the state using the memoized state and the update queue. + var updateQueue = workInProgress.updateQueue; + var previousState = current ? current.memoizedState : null; + var state = updateQueue ? + mergeUpdateQueue(updateQueue, previousState, props) : + previousState; + var instance = workInProgress.stateNode; if (!instance) { 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); - } + state = instance.state || null; + // The initial state must be added to the update queue in case + // setState is called before the initial render. + if (state !== null) { + workInProgress.updateQueue = createUpdateQueue(state); } - if (!workInProgress.childInProgress && workInProgress.memoizedProps) { + // The instance needs access to the fiber so that it can schedule updates + ReactInstanceMap.set(instance, workInProgress); + instance.updater = updater; + } else if (typeof instance.shouldComponentUpdate === 'function' && + !(updateQueue && updateQueue.isForced)) { + 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. instance.props = workInProgress.memoizedProps; - if (!instance.shouldComponentUpdate(props)) { + instance.state = workInProgress.memoizedState; + if (!instance.shouldComponentUpdate(props, state)) { return bailoutOnAlreadyFinishedWork(current, workInProgress); } } } + instance.props = props; + instance.state = state; 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; - - 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. + const nextChildren = workInProgress.pendingProps.children; + if (workInProgress.pendingProps.hidden && + workInProgress.pendingUpdatePriority !== OffscreenPriority) { + // If this host component is hidden, we can bail out on the children. + // We'll rerender the children later at the lower priority. + + // It is unfortunate that we have to do the reconciliation of these + // children already since that will add them to the tree even though + // they are not actually done yet. If this is a large set it is also + // confusing that this takes time to do right now instead of later. + + if (workInProgress.progressedPriority === OffscreenPriority) { + // If we already made some progress on the offscreen priority before, + // then we should continue from where we left off. + workInProgress.child = workInProgress.progressedChild; + } + + // Reconcile the children and stash them for later work. reconcileChildrenAtPriority(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; } } @@ -153,7 +295,7 @@ module.exports = function(config : HostConfig) { } } reconcileChildren(current, workInProgress, value); - workInProgress.pendingWorkPriority = NoWork; + return workInProgress.child; } function updateCoroutineComponent(current, workInProgress) { @@ -162,29 +304,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 +323,82 @@ 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 { + const priorityLevel = workInProgress.pendingUpdatePriority; + + // 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; + // } + + cloneChildFibers(workInProgress); + markChildAsProgressed(current, workInProgress, priorityLevel); + return workInProgress.child; } - 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; - - 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); - } + 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 && + workInProgress.updateQueue === null + )) { 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 +408,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 c7a9f2f7109..c654a0065c2 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -22,6 +22,7 @@ var { HostContainer, HostComponent, } = ReactTypeOfWork; +var { callCallbacks } = require('ReactFiberUpdateQueue'); module.exports = function(config : HostConfig) { @@ -31,6 +32,18 @@ module.exports = function(config : HostConfig) { function commitWork(finishedWork : Fiber) : void { switch (finishedWork.tag) { case ClassComponent: { + // Clear updates from current fiber. This must go before the callbacks + // are reset, in case an update is triggered from inside a callback. Is + // this safe? Relies on the assumption that work is only committed if + // the update queue is empty. + if (finishedWork.alternate) { + finishedWork.alternate.updateQueue = null; + } + if (finishedWork.callbackList) { + const { callbackList } = finishedWork; + finishedWork.callbackList = null; + callCallbacks(callbackList, finishedWork.stateNode); + } // TODO: Fire componentDidMount/componentDidUpdate, update refs return; } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 53421116b48..38c257b05be 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -46,7 +46,6 @@ module.exports = function(config : HostConfig) { } } - /* // TODO: It's possible this will create layout thrash issues because mutations // of the DOM and life-cycles are interleaved. E.g. if a componentDidMount // of a sibling reads, then the next sibling updates and reads etc. @@ -59,7 +58,6 @@ module.exports = function(config : HostConfig) { } workInProgress.lastEffect = workInProgress; } - */ function transferOutput(child : ?Fiber, returnFiber : Fiber) { // If we have a single result, we just pass that through as the output to @@ -115,7 +113,7 @@ module.exports = function(config : HostConfig) { var currentFirstChild = current ? current.stateNode : null; // Inherit the priority of the returnFiber. - const priority = workInProgress.pendingWorkPriority; + const priority = workInProgress.pendingUpdatePriority; workInProgress.stateNode = reconcileChildFibers( workInProgress, currentFirstChild, @@ -132,6 +130,17 @@ module.exports = function(config : HostConfig) { return null; case ClassComponent: transferOutput(workInProgress.child, workInProgress); + // Don't use the state queue to compute the memoized state. We already + // merged it and assigned it to the instance. Transfer it from there. + // Also need to transfer the props, because pendingProps will be null + // in the case of an update + const { state, props } = workInProgress.stateNode; + workInProgress.memoizedState = state; + workInProgress.memoizedProps = props; + // Transfer update queue to callbackList field so callbacks can be + // called during commit phase. + workInProgress.callbackList = workInProgress.updateQueue; + markForPostEffect(workInProgress); return null; case HostContainer: transferOutput(workInProgress.child, workInProgress); @@ -162,10 +171,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 eb61c4595ff..00000000000 --- 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 4ece60a0f2e..623f6c98b31 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -70,7 +70,10 @@ 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.pendingUpdatePriority = LowPriority; container.pendingWorkPriority = LowPriority; scheduleLowPriWork(root, LowPriority); @@ -86,6 +89,7 @@ module.exports = function(config : HostConfig) : Reconci const root : FiberRoot = (container.stateNode : any); // TODO: Use pending work/state instead of props. root.current.pendingProps = element; + root.current.pendingUpdatePriority = LowPriority; root.current.pendingWorkPriority = LowPriority; scheduleLowPriWork(root, LowPriority); @@ -96,6 +100,7 @@ module.exports = function(config : HostConfig) : Reconci const root : FiberRoot = (container.stateNode : any); // TODO: Use pending work/state instead of props. root.current.pendingProps = []; + root.current.pendingUpdatePriority = LowPriority; root.current.pendingWorkPriority = LowPriority; scheduleLowPriWork(root, LowPriority); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index b6e71289689..54e8708951a 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -22,20 +22,26 @@ 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; +export type Scheduler = { + scheduleLowPriWork: (root : FiberRoot, priority : PriorityLevel) => void +}; + module.exports = function(config : HostConfig) { + // Use a closure to circumvent the circular dependency between the scheduler + // and ReactFiberBeginWork. Don't know if there's a better way to do this. + let scheduler; + function getScheduler(): Scheduler { + return scheduler; + } - const { beginWork } = ReactFiberBeginWork(config); + const { beginWork } = ReactFiberBeginWork(config, getScheduler); const { completeWork } = ReactFiberCompleteWork(config); const { commitWork } = ReactFiberCommitWork(config); @@ -65,33 +71,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.pendingUpdatePriority) { + highestPriorityLevel = root.current.pendingUpdatePriority; + 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; @@ -114,6 +110,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,19 +137,17 @@ module.exports = function(config : HostConfig) { const current = workInProgress.alternate; const next = completeWork(current, workInProgress); + workInProgress.pendingUpdatePriority = NoWork; + 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; + workInProgress.updateQueue = null; 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 +171,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 +201,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; @@ -263,7 +270,8 @@ module.exports = function(config : HostConfig) { } */ - return { + scheduler = { scheduleLowPriWork: scheduleLowPriWork, }; + return scheduler; }; diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js new file mode 100644 index 00000000000..aab252b6558 --- /dev/null +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -0,0 +1,89 @@ +/** + * 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 ReactFiberUpdateQueue + * @flow + */ + +'use strict'; + +type UpdateQueueNode = { + partialState: any, + callback: ?Function, + callbackWasCalled: boolean, + next: ?UpdateQueueNode, +}; + +export type UpdateQueue = UpdateQueueNode & { + isReplace: boolean, + isForced: boolean, + tail: UpdateQueueNode +}; + +exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { + const queue = { + partialState, + callback: null, + callbackWasCalled: false, + next: null, + isReplace: false, + isForced: false, + tail: (null : any), + }; + queue.tail = queue; + return queue; +}; + +exports.addToQueue = function(queue : UpdateQueue, partialState : mixed) : UpdateQueue { + const node = { + partialState, + callback: null, + callbackWasCalled: false, + next: null, + }; + queue.tail.next = node; + queue.tail = node; + return queue; +}; + +exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : UpdateQueue { + if (queue.tail.callback) { + // If the tail already as a callback, add an empty node to queue + exports.addToQueue(queue, null); + } + queue.tail.callback = callback; + return queue; +}; + +exports.callCallbacks = function(queue : UpdateQueue, context : any) { + let node : ?UpdateQueueNode = queue; + while (node) { + if (node.callback && !node.callbackWasCalled) { + node.callbackWasCalled = true; + node.callback.call(context); + } + node = node.next; + } +}; + +exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props : any) : any { + let node : ?UpdateQueueNode = queue; + let state = queue.isReplace ? null : Object.assign({}, prevState); + while (node) { + let partialState; + if (typeof node.partialState === 'function') { + const updateFn = node.partialState; + partialState = updateFn(state, props); + } else { + partialState = node.partialState; + } + state = Object.assign(state || {}, partialState); + node = node.next; + } + return state; +}; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index 65d3b09e0ab..42cd8b09720 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', function() { // 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', function() { // 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', function() { 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 = []; @@ -556,4 +556,253 @@ describe('ReactIncremental', function() { expect(ops).toEqual(['Content', 'Bar', 'Middle']); }); + + it('can update in the middle of a tree using setState', () => { + let instance; + class Bar extends React.Component { + constructor() { + super(); + this.state = { a: 'a' }; + instance = this; + } + render() { + return
{this.props.children}
; + } + } + + function Foo() { + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(instance.state).toEqual({ a: 'a' }); + instance.setState({ b: 'b' }); + ReactNoop.flush(); + expect(instance.state).toEqual({ a: 'a', b: 'b' }); + }); + + it('can queue multiple state updates', () => { + let instance; + class Bar extends React.Component { + constructor() { + super(); + this.state = { a: 'a' }; + instance = this; + } + render() { + return
{this.props.children}
; + } + } + + function Foo() { + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + // Call setState multiple times before flushing + instance.setState({ b: 'b' }); + instance.setState({ c: 'c' }); + instance.setState({ d: 'd' }); + ReactNoop.flush(); + expect(instance.state).toEqual({ a: 'a', b: 'b', c: 'c', d: 'd' }); + }); + + it('can use updater form of setState', () => { + let instance; + class Bar extends React.Component { + constructor() { + super(); + this.state = { num: 1 }; + instance = this; + } + render() { + return
{this.props.children}
; + } + } + + function Foo({ multiplier }) { + return ( +
+ +
+ ); + } + + function updater(state, props) { + return { num: state.num * props.multiplier }; + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(instance.state.num).toEqual(1); + instance.setState(updater); + ReactNoop.flush(); + expect(instance.state.num).toEqual(2); + instance.setState(updater); + ReactNoop.render(); + ReactNoop.flush(); + expect(instance.state.num).toEqual(6); + }); + + it('can call setState inside update callback', () => { + let instance; + class Bar extends React.Component { + constructor() { + super(); + this.state = { num: 1 }; + instance = this; + } + render() { + return
{this.props.children}
; + } + } + + function Foo({ multiplier }) { + return ( +
+ +
+ ); + } + + function updater(state, props) { + return { num: state.num * props.multiplier }; + } + + function callback() { + this.setState({ called: true }); + } + + ReactNoop.render(); + ReactNoop.flush(); + instance.setState(updater); + instance.setState(updater, callback); + ReactNoop.flush(); + expect(instance.state.num).toEqual(4); + expect(instance.state.called).toEqual(true); + }); + + it('can setState without overriding its parents\' priority', () => { + let ops = []; + let instance; + class Baz extends React.Component { + constructor() { + super(); + instance = this; + this.state = { num: 0 }; + } + render() { + ops.push('Baz'); + return ; + } + } + + function Bar({ id }) { + ops.push('Bar' + id); + return
; + } + + function Foo() { + ops.push('Foo'); + return [ + , + , + ]; + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar2', 'Bar1', 'Baz']); + ops = []; + ReactNoop.render(); + // Even though Baz is in a hidden subtree, calling setState gives it a + // higher priority. It should not affect the priority of anything else in + // the subtree. + instance.setState({ num: 1 }); + ReactNoop.flush(); + // Baz should come before Bar1 because it has higher priority + expect(ops).toEqual(['Foo', 'Bar2', 'Baz', 'Bar1']); + }); + + it('can replaceState', () => { + let instance; + const Bar = React.createClass({ + getInitialState() { + instance = this; + return { a: 'a' }; + }, + render() { + return
{this.props.children}
; + }, + }); + + function Foo() { + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + instance.setState({ b: 'b' }); + instance.setState({ c: 'c' }); + instance.replaceState({ d: 'd' }); + ReactNoop.flush(); + expect(instance.state).toEqual({ d: 'd' }); + }); + + it('can forceUpdate', () => { + const ops = []; + + function Baz() { + ops.push('Baz'); + return
; + } + + let instance; + class Bar extends React.Component { + constructor() { + super(); + instance = this; + } + shouldComponentUpdate() { + return false; + } + render() { + ops.push('Bar'); + return ; + } + } + + function Foo() { + ops.push('Foo'); + return ( +
+ +
+ ); + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar', 'Baz']); + instance.forceUpdate(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo', 'Bar', 'Baz', 'Bar', 'Baz']); + }); }); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index c6068855610..5813d058f2d 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', function() { // 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,8 +270,118 @@ describe('ReactIncrementalSideEffects', function() { ]); }); + 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? + it('calls callback after update is flushed', () => { + let instance; + class Foo extends React.Component { + constructor() { + super(); + instance = this; + this.state = { text: 'foo' }; + } + render() { + return ; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.root.children).toEqual([ + span('foo'), + ]); + let called = false; + instance.setState({ text: 'bar' }, () => { + expect(ReactNoop.root.children).toEqual([ + span('bar'), + ]); + called = true; + }); + ReactNoop.flush(); + expect(called).toBe(true); + }); + + // TODO: Test that callbacks are not lost if an update is preempted. }); diff --git a/src/renderers/shared/stack/reconciler/ReactInstanceMap.js b/src/renderers/shared/shared/ReactInstanceMap.js similarity index 100% rename from src/renderers/shared/stack/reconciler/ReactInstanceMap.js rename to src/renderers/shared/shared/ReactInstanceMap.js