diff --git a/scripts/fiber/tests-failing.txt b/scripts/fiber/tests-failing.txt index 8c9b7a49041c..955d305f69e0 100644 --- a/scripts/fiber/tests-failing.txt +++ b/scripts/fiber/tests-failing.txt @@ -369,7 +369,6 @@ src/renderers/dom/stack/client/__tests__/ReactMount-test.js * should account for escaping on a checksum mismatch * should warn if render removes React-rendered children * should warn if the unmounted node was rendered by another copy of React -* passes the correct callback context * tracks root instances * marks top-level mounts @@ -608,8 +607,6 @@ src/renderers/shared/stack/reconciler/__tests__/ReactUpdates-test.js * throws in setState if the update callback is not a function * throws in replaceState if the update callback is not a function * throws in forceUpdate if the update callback is not a function -* does not update one component twice in a batch (#2410) -* unstable_batchedUpdates should return value from a callback src/renderers/shared/stack/reconciler/__tests__/refs-test.js * Should increase refs with an increase in divs diff --git a/scripts/fiber/tests-passing.txt b/scripts/fiber/tests-passing.txt index 995e16d47ef1..b2d06adb7274 100644 --- a/scripts/fiber/tests-passing.txt +++ b/scripts/fiber/tests-passing.txt @@ -692,6 +692,7 @@ src/renderers/dom/stack/client/__tests__/ReactMount-test.js * should warn if mounting into dirty rendered markup * should not warn if mounting into non-empty node * should warn when mounting into document.body +* passes the correct callback context src/renderers/dom/stack/client/__tests__/ReactMountDestruction-test.js * should destroy a react root upon request @@ -788,6 +789,8 @@ src/renderers/shared/fiber/__tests__/ReactIncremental-test.js * calls componentWill* twice if an update render is aborted * does not call componentWillReceiveProps for state-only updates * skips will/DidUpdate when bailing unless an update was already in progress +* performs batched updates at the end of the batch +* can nest batchedUpdates src/renderers/shared/fiber/__tests__/ReactIncrementalReflection-test.js * handles isMounted even when the initial render is deferred @@ -833,6 +836,7 @@ src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js * can defer side-effects and reuse them later - complex * deprioritizes setStates that happens within a deprioritized tree * calls callback after update is flushed +* calls setState callback even if component bails out * calls componentWillUnmount after a deletion, even if nested * calls componentDidMount/Update after insertion/update * invokes ref callbacks after insertion/update/unmount @@ -1124,7 +1128,9 @@ src/renderers/shared/stack/reconciler/__tests__/ReactUpdates-test.js * should not reconcile children passed via props * should queue nested updates * calls componentWillReceiveProps setState callback properly +* does not update one component twice in a batch (#2410) * does not update one component twice in a batch (#6371) +* unstable_batchedUpdates should return value from a callback src/renderers/shared/stack/reconciler/__tests__/Transaction-test.js * should invoke closers with/only-with init returns diff --git a/src/addons/transitions/__tests__/ReactTransitionGroup-test.js b/src/addons/transitions/__tests__/ReactTransitionGroup-test.js index 90c0e0190066..0729be98f46e 100644 --- a/src/addons/transitions/__tests__/ReactTransitionGroup-test.js +++ b/src/addons/transitions/__tests__/ReactTransitionGroup-test.js @@ -97,10 +97,10 @@ describe('ReactTransitionGroup', () => { expect(log).toEqual(['didMount', 'willEnter', 'didEnter']); log = []; - instance.setState({count: 1}, function() { - expect(log).toEqual(['willLeave', 'didLeave', 'willUnmount']); - }); + instance.setState({count: 1}); }); + + expect(log).toEqual(['willLeave', 'didLeave', 'willUnmount']); }); it('should handle enter/leave/enter/leave correctly', () => { diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 8294883db55b..08fd6d7c59df 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -112,6 +112,8 @@ var DOMRenderer = ReactFiberReconciler({ scheduleDeferredCallback: window.requestIdleCallback, + useSyncScheduling: true, + }); var warned = false; @@ -163,6 +165,10 @@ var ReactDOM = { return DOMRenderer.findHostInstance(component); }, + unstable_batchedUpdates(fn : () => A) : A { + return DOMRenderer.batchedUpdates(fn); + }, + }; module.exports = ReactDOM; diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index ea0b4ec655f7..f57bf2b9c82f 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -257,6 +257,10 @@ var ReactNoop = { NoopRenderer.performWithPriority(AnimationPriority, fn); }, + batchedUpdates: NoopRenderer.batchedUpdates, + + syncUpdates: NoopRenderer.syncUpdates, + // Logs the current state of the tree. dumpTree(rootID : string = DEFAULT_ROOT_ID) { const root = roots.get(rootID); diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index bcc8958e5422..15ba59a79434 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -15,7 +15,7 @@ import type { ReactCoroutine } from 'ReactCoroutine'; import type { Fiber } from 'ReactFiber'; import type { HostConfig } from 'ReactFiberReconciler'; -import type { PriorityLevel } from 'ReactPriorityLevel'; +import type { PriorityLevel } from 'ReactPriorityLevel'; import ReactCurrentOwner from 'ReactCurrentOwner'; var { @@ -48,7 +48,7 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent'); module.exports = function( config : HostConfig, - scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void + scheduleUpdate : (fiber: Fiber) => void ) { const { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 2211dead1876..df1ceaa3bec1 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -13,10 +13,8 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; -import type { PriorityLevel } from 'ReactPriorityLevel'; import type { UpdateQueue } from 'ReactFiberUpdateQueue'; -var { LowPriority } = require('ReactPriorityLevel'); var { createUpdateQueue, addToQueue, @@ -27,16 +25,16 @@ var { isMounted } = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); var shallowEqual = require('shallowEqual'); -module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void) { +module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { - function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue, priorityLevel : PriorityLevel) { + function scheduleUpdateQueue(fiber: Fiber, updateQueue: UpdateQueue) { 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; } - scheduleUpdate(fiber, priorityLevel); + scheduleUpdate(fiber); } // Class component state updater @@ -47,19 +45,19 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori const updateQueue = fiber.updateQueue ? addToQueue(fiber.updateQueue, partialState) : createUpdateQueue(partialState); - scheduleUpdateQueue(fiber, updateQueue, LowPriority); + scheduleUpdateQueue(fiber, updateQueue); }, enqueueReplaceState(instance, state) { const fiber = ReactInstanceMap.get(instance); const updateQueue = createUpdateQueue(state); updateQueue.isReplace = true; - scheduleUpdateQueue(fiber, updateQueue, LowPriority); + scheduleUpdateQueue(fiber, updateQueue); }, enqueueForceUpdate(instance) { const fiber = ReactInstanceMap.get(instance); const updateQueue = fiber.updateQueue || createUpdateQueue(null); updateQueue.isForced = true; - scheduleUpdateQueue(fiber, updateQueue, LowPriority); + scheduleUpdateQueue(fiber, updateQueue); }, enqueueCallback(instance, callback) { const fiber = ReactInstanceMap.get(instance); @@ -67,10 +65,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori fiber.updateQueue : createUpdateQueue(null); addCallbackToQueue(updateQueue, callback); - fiber.updateQueue = updateQueue; - if (fiber.alternate) { - fiber.alternate.updateQueue = updateQueue; - } + scheduleUpdateQueue(fiber, updateQueue); }, }; @@ -131,7 +126,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // process them now. const updateQueue = workInProgress.updateQueue; if (updateQueue) { - instance.state = mergeUpdateQueue(updateQueue, state, props); + instance.state = mergeUpdateQueue(updateQueue, instance, state, props); } } } @@ -175,7 +170,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // process them now. const newUpdateQueue = workInProgress.updateQueue; if (newUpdateQueue) { - newInstance.state = mergeUpdateQueue(newUpdateQueue, newState, newProps); + newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps); } } return true; @@ -212,11 +207,21 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : Priori // TODO: Previous state can be null. let newState; if (updateQueue) { - newState = mergeUpdateQueue(updateQueue, previousState, newProps); + if (!updateQueue.hasUpdate) { + newState = previousState; + } else { + newState = mergeUpdateQueue(updateQueue, instance, previousState, newProps); + } } else { newState = previousState; } + if (oldProps === newProps && + previousState === newState && + updateQueue && !updateQueue.isForced) { + return false; + } + if (!checkShouldComponentUpdate( workInProgress, oldProps, diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index db844453c8ef..b23e5bdd6f8d 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -29,7 +29,8 @@ var { callCallbacks } = require('ReactFiberUpdateQueue'); var { Placement, - PlacementAndUpdate, + Update, + Callback, } = require('ReactTypeOfSideEffect'); module.exports = function(config : HostConfig) { @@ -102,8 +103,7 @@ module.exports = function(config : HostConfig) { // If it is not host node and, we might have a host node inside it. // Try to search down until we find one. // TODO: For coroutines, this will have to search the stateNode. - if (node.effectTag === Placement || - node.effectTag === PlacementAndUpdate) { + if (node.effectTag & Placement) { // If we don't have a child, try the siblings instead. continue siblings; } @@ -114,8 +114,7 @@ module.exports = function(config : HostConfig) { } } // Check if this host node is stable or about to be placed. - if (node.effectTag !== Placement && - node.effectTag !== PlacementAndUpdate) { + if (!(node.effectTag & Placement)) { // Found it! return node.stateNode; } @@ -329,41 +328,41 @@ module.exports = function(config : HostConfig) { case ClassComponent: { const instance = finishedWork.stateNode; let error = null; - if (!current) { - if (typeof instance.componentDidMount === 'function') { - error = tryCallComponentDidMount(instance); - } - } else { - if (typeof instance.componentDidUpdate === 'function') { - const prevProps = current.memoizedProps; - const prevState = current.memoizedState; - error = tryCallComponentDidUpdate(instance, prevProps, prevState); + if (finishedWork.effectTag & Update) { + if (!current) { + if (typeof instance.componentDidMount === 'function') { + error = tryCallComponentDidMount(instance); + } + } else { + if (typeof instance.componentDidUpdate === 'function') { + const prevProps = current.memoizedProps; + const prevState = current.memoizedState; + error = tryCallComponentDidUpdate(instance, prevProps, prevState); + } } + attachRef(current, finishedWork, instance); } - // 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. + // Clear updates from current fiber. if (finishedWork.alternate) { finishedWork.alternate.updateQueue = null; } - if (finishedWork.callbackList) { - const { callbackList } = finishedWork; - finishedWork.callbackList = null; - callCallbacks(callbackList, instance); + if (finishedWork.effectTag & Callback) { + if (finishedWork.callbackList) { + callCallbacks(finishedWork.callbackList, instance); + finishedWork.callbackList = null; + } } - attachRef(current, finishedWork, instance); if (error) { return trapError(finishedWork, error); } return null; } case HostContainer: { - const instance = finishedWork.stateNode; - if (instance.callbackList) { - const { callbackList } = instance; - instance.callbackList = null; - callCallbacks(callbackList, instance.current.child.stateNode); + const rootFiber = finishedWork.stateNode; + if (rootFiber.callbackList) { + const { callbackList } = rootFiber; + rootFiber.callbackList = null; + callCallbacks(callbackList, rootFiber.current.child.stateNode); } } case HostComponent: { diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index 590332d5ce19..b0eea7c87e25 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -34,6 +34,7 @@ var { } = ReactTypeOfWork; var { Update, + Callback, } = ReactTypeOfSideEffect; module.exports = function(config : HostConfig) { @@ -48,6 +49,11 @@ module.exports = function(config : HostConfig) { workInProgress.effectTag |= Update; } + function markCallback(workInProgress : Fiber) { + // Tag the fiber with a callback effect. + workInProgress.effectTag |= Callback; + } + function transferOutput(child : ?Fiber, returnFiber : Fiber) { // If we have a single result, we just pass that through as the output to // avoid unnecessary traversal. When we have multiple output, we just pass @@ -124,19 +130,24 @@ module.exports = function(config : HostConfig) { // Also need to transfer the props, because pendingProps will be null // in the case of an update const { state, props } = workInProgress.stateNode; + const updateQueue = workInProgress.updateQueue; workInProgress.memoizedState = state; workInProgress.memoizedProps = props; - // Transfer update queue to callbackList field so callbacks can be - // called during commit phase. - workInProgress.callbackList = workInProgress.updateQueue; if (current) { if (current.memoizedProps !== workInProgress.memoizedProps || - current.memoizedState !== workInProgress.memoizedState) { + current.memoizedState !== workInProgress.memoizedState || + updateQueue && updateQueue.isForced) { markUpdate(workInProgress); } } else { markUpdate(workInProgress); } + if (updateQueue && updateQueue.hasCallback) { + // Transfer update queue to callbackList field so callbacks can be + // called during commit phase. + workInProgress.callbackList = updateQueue; + markCallback(workInProgress); + } return null; case HostContainer: transferOutput(workInProgress.child, workInProgress); diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index d5995a3c65b0..e85f25bc469d 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -28,7 +28,7 @@ if (__DEV__) { var { findCurrentHostFiber } = require('ReactFiberTreeReflection'); -type Deadline = { +export type Deadline = { timeRemaining : () => number }; @@ -56,8 +56,9 @@ export type HostConfig = { removeChild(parentInstance : I, child : I | TI) : void, scheduleAnimationCallback(callback : () => void) : void, - scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void + scheduleDeferredCallback(callback : (deadline : Deadline) => void) : void, + useSyncScheduling ?: boolean, }; type OpaqueNode = Fiber; @@ -67,6 +68,11 @@ export type Reconciler = { updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, performWithPriority(priorityLevel : PriorityLevel, fn : Function) : void, + /* eslint-disable no-undef */ + // FIXME: ESLint complains about type parameter + batchedUpdates(fn : () => A) : A, + syncUpdates(fn : () => A) : A, + /* eslint-enable no-undef */ // Used to extract the return value from the initial render. Legacy API. getPublicRootInstance(container : OpaqueNode) : (ReactComponent | TI | I | null), @@ -77,7 +83,12 @@ export type Reconciler = { module.exports = function(config : HostConfig) : Reconciler { - var { scheduleWork, performWithPriority } = ReactFiberScheduler(config); + var { + scheduleWork, + performWithPriority, + batchedUpdates, + syncUpdates, + } = ReactFiberScheduler(config); return { @@ -141,6 +152,10 @@ module.exports = function(config : HostConfig) : performWithPriority, + batchedUpdates, + + syncUpdates, + getPublicRootInstance(container : OpaqueNode) : (ReactComponent | I | TI | null) { const root : FiberRoot = (container.stateNode : any); const containerFiber = root.current; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index fd236dbfce55..fb4e5e885b12 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -15,7 +15,7 @@ import type { TrappedError } from 'ReactFiberErrorBoundary'; import type { Fiber } from 'ReactFiber'; import type { FiberRoot } from 'ReactFiberRoot'; -import type { HostConfig } from 'ReactFiberReconciler'; +import type { HostConfig, Deadline } from 'ReactFiberReconciler'; import type { PriorityLevel } from 'ReactPriorityLevel'; var ReactFiberBeginWork = require('ReactFiberBeginWork'); @@ -39,6 +39,11 @@ var { Update, PlacementAndUpdate, Deletion, + Callback, + PlacementAndCallback, + UpdateAndCallback, + PlacementAndUpdateAndCallback, + DeletionAndCallback, } = require('ReactTypeOfSideEffect'); var { @@ -52,9 +57,6 @@ if (__DEV__) { var timeHeuristicForUnitOfWork = 1; 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. - const { beginWork } = ReactFiberBeginWork(config, scheduleUpdate); const { completeWork } = ReactFiberCompleteWork(config); const { commitInsertion, commitDeletion, commitWork, commitLifeCycles } = @@ -62,9 +64,15 @@ module.exports = function(config : HostConfig) { const scheduleAnimationCallback = config.scheduleAnimationCallback; const scheduleDeferredCallback = config.scheduleDeferredCallback; + const useSyncScheduling = config.useSyncScheduling; + + // The priority level to use when scheduling an update. + let priorityContext : PriorityLevel = useSyncScheduling ? + SynchronousPriority : + LowPriority; - // The default priority to use for updates. - let defaultPriority : PriorityLevel = LowPriority; + // Whether updates should be batched. Only applies when using sync scheduling. + let shouldBatchUpdates : boolean = false; // The next work in progress fiber that we're currently working on. let nextUnitOfWork : ?Fiber = null; @@ -133,28 +141,34 @@ module.exports = function(config : HostConfig) { let effectfulFiber = finishedWork.firstEffect; while (effectfulFiber) { switch (effectfulFiber.effectTag) { - case Placement: { + case Placement: + case PlacementAndCallback: { commitInsertion(effectfulFiber); - // Clear the effect tag so that we know that this is inserted, before + // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. effectfulFiber.effectTag = NoEffect; break; } - case PlacementAndUpdate: { + case PlacementAndUpdate: + case PlacementAndUpdateAndCallback: { + // Placement commitInsertion(effectfulFiber); - const current = effectfulFiber.alternate; - commitWork(current, effectfulFiber); // Clear the "placement" from effect tag so that we know that this is inserted, before // any life-cycles like componentDidMount gets called. effectfulFiber.effectTag = Update; + + // Update + const current = effectfulFiber.alternate; + commitWork(current, effectfulFiber); break; } - case Update: { + case Update: + case UpdateAndCallback: const current = effectfulFiber.alternate; commitWork(current, effectfulFiber); break; - } - case Deletion: { + case Deletion: + case DeletionAndCallback: // Deletion might cause an error in componentWillUnmount(). // We will continue nevertheless and handle those later on. const trappedErrors = commitDeletion(effectfulFiber); @@ -170,8 +184,8 @@ module.exports = function(config : HostConfig) { } } break; - } } + effectfulFiber = effectfulFiber.nextEffect; } @@ -180,8 +194,7 @@ module.exports = function(config : HostConfig) { // already been invoked. effectfulFiber = finishedWork.firstEffect; while (effectfulFiber) { - if (effectfulFiber.effectTag === Update || - effectfulFiber.effectTag === PlacementAndUpdate) { + if (effectfulFiber.effectTag & (Update | Callback)) { const current = effectfulFiber.alternate; const trappedError = commitLifeCycles(current, effectfulFiber); if (trappedError) { @@ -373,20 +386,7 @@ module.exports = function(config : HostConfig) { } function performDeferredWork(deadline) { - try { - performDeferredWorkUnsafe(deadline); - } catch (error) { - const failedUnitOfWork = nextUnitOfWork; - // Reset because it points to the error boundary: - nextUnitOfWork = null; - if (!failedUnitOfWork) { - // We shouldn't end up here because nextUnitOfWork - // should always be set while work is being performed. - throw error; - } - const trappedError = trapError(failedUnitOfWork, error); - handleErrors([trappedError]); - } + performAndHandleErrors(LowPriority, deadline); } function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { @@ -425,35 +425,21 @@ module.exports = function(config : HostConfig) { // Always start from the root nextUnitOfWork = findNextUnitOfWork(); while (nextUnitOfWork && - nextPriorityLevel !== NoWork) { + nextPriorityLevel !== NoWork && + nextPriorityLevel <= AnimationPriority) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); if (!nextUnitOfWork) { // Keep searching for animation work until there's no more left nextUnitOfWork = findNextUnitOfWork(); } - // Stop if the next unit of work is low priority - if (nextPriorityLevel > AnimationPriority) { - scheduleDeferredCallback(performDeferredWork); - return; - } + } + if (nextUnitOfWork && nextPriorityLevel > AnimationPriority) { + scheduleDeferredCallback(performDeferredWork); } } function performAnimationWork() { - try { - performAnimationWorkUnsafe(); - } catch (error) { - const failedUnitOfWork = nextUnitOfWork; - // Reset because it points to the error boundary: - nextUnitOfWork = null; - if (!failedUnitOfWork) { - // We shouldn't end up here because nextUnitOfWork - // should always be set while work is being performed. - throw error; - } - const trappedError = trapError(failedUnitOfWork, error); - handleErrors([trappedError]); - } + performAndHandleErrors(AnimationPriority); } function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { @@ -506,6 +492,91 @@ module.exports = function(config : HostConfig) { return root; } + function performSynchronousWorkUnsafe() { + // Perform work now + nextUnitOfWork = findNextUnitOfWork(); + while (nextUnitOfWork && + nextPriorityLevel === SynchronousPriority) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); + + if (!nextUnitOfWork) { + nextUnitOfWork = findNextUnitOfWork(); + } + } + if (nextUnitOfWork) { + if (nextPriorityLevel > AnimationPriority) { + scheduleDeferredCallback(performDeferredWork); + return; + } + scheduleAnimationCallback(performAnimationWork); + } + } + + function performSynchronousWork() { + const prev = shouldBatchUpdates; + shouldBatchUpdates = true; + // All nested updates are batched + try { + performAndHandleErrors(SynchronousPriority); + } finally { + shouldBatchUpdates = prev; + } + } + + function scheduleSynchronousWork(root : FiberRoot) { + root.current.pendingWorkPriority = SynchronousPriority; + + if (root.isScheduled) { + // If we're already scheduled, we can bail out. + return; + } + root.isScheduled = true; + if (lastScheduledRoot) { + // Schedule ourselves to the end. + lastScheduledRoot.nextScheduledRoot = root; + lastScheduledRoot = root; + } else { + // We're the only work scheduled. + nextScheduledRoot = root; + lastScheduledRoot = root; + + if (!shouldBatchUpdates) { + // Unless in batched mode, perform work immediately + performSynchronousWork(); + } + } + } + + function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : null | Deadline) { + // The exact priority level doesn't matter, so long as it's in range of the + // work (sync, animation, deferred) being performed. + try { + if (priorityLevel === SynchronousPriority) { + performSynchronousWorkUnsafe(); + } else if (priorityLevel > AnimationPriority) { + if (!deadline) { + throw new Error('No deadline'); + } else { + performDeferredWorkUnsafe(deadline); + } + return; + } else { + performAnimationWorkUnsafe(); + } + } catch (error) { + const failedUnitOfWork = nextUnitOfWork; + // Reset because it points to the error boundary: + nextUnitOfWork = null; + if (!failedUnitOfWork) { + // We shouldn't end up here because nextUnitOfWork + // should always be set while work is being performed. + throw error; + } + const trappedError = trapError(failedUnitOfWork, error); + handleErrors([trappedError]); + } + } + function handleErrors(initialTrappedErrors : Array) : void { let nextTrappedErrors = initialTrappedErrors; let firstUncaughtError = null; @@ -542,10 +613,7 @@ module.exports = function(config : HostConfig) { // We will process an update caused by each error boundary synchronously. affectedBoundaries.forEach(boundary => { - // FIXME: We only specify LowPriority here so that setState() calls from the error - // boundaries are respected. Instead we should set default priority level or something - // like this. Reconsider this piece when synchronous scheduling is in place. - const priority = LowPriority; + const priority = priorityContext; const root = scheduleErrorBoundaryWork(boundary, priority); // This should use findNextUnitOfWork() when synchronous scheduling is implemented. let fiber = cloneFiber(root.current, priority); @@ -579,21 +647,24 @@ module.exports = function(config : HostConfig) { } function scheduleWork(root : FiberRoot) { - if (defaultPriority === SynchronousPriority) { - throw new Error('Not implemented yet'); - } + scheduleWorkAtPriority(root, priorityContext); + } - if (defaultPriority === NoWork) { + function scheduleWorkAtPriority(root : FiberRoot, priorityLevel : PriorityLevel) { + if (priorityLevel === NoWork) { return; - } - if (defaultPriority > AnimationPriority) { - scheduleDeferredWork(root, defaultPriority); + } else if (priorityLevel === SynchronousPriority) { + scheduleSynchronousWork(root); + } else if (priorityLevel <= AnimationPriority) { + scheduleAnimationWork(root, priorityLevel); + } else { + scheduleDeferredWork(root, priorityLevel); return; } - scheduleAnimationWork(root, defaultPriority); } - function scheduleUpdate(fiber: Fiber, priorityLevel : PriorityLevel): void { + function scheduleUpdate(fiber : Fiber) { + const priorityLevel = priorityContext; while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { @@ -608,7 +679,7 @@ module.exports = function(config : HostConfig) { if (!fiber.return) { if (fiber.tag === HostContainer) { const root : FiberRoot = (fiber.stateNode : any); - scheduleDeferredWork(root, priorityLevel); + scheduleWorkAtPriority(root, priorityLevel); return; } else { throw new Error('Invalid root'); @@ -619,12 +690,36 @@ module.exports = function(config : HostConfig) { } function performWithPriority(priorityLevel : PriorityLevel, fn : Function) { - const previousDefaultPriority = defaultPriority; - defaultPriority = priorityLevel; + const previousPriorityContext = priorityContext; + priorityContext = priorityLevel; try { fn(); } finally { - defaultPriority = previousDefaultPriority; + priorityContext = previousPriorityContext; + } + } + + function batchedUpdates(fn : () => A) : A { + const prev = shouldBatchUpdates; + shouldBatchUpdates = true; + try { + return fn(); + } finally { + shouldBatchUpdates = prev; + // If we've exited the batch, perform any scheduled sync work + if (!shouldBatchUpdates) { + performSynchronousWork(); + } + } + } + + function syncUpdates(fn : () => A) : A { + const previousPriorityContext = priorityContext; + priorityContext = SynchronousPriority; + try { + return fn(); + } finally { + priorityContext = previousPriorityContext; } } @@ -632,5 +727,7 @@ module.exports = function(config : HostConfig) { scheduleWork: scheduleWork, scheduleDeferredWork: scheduleDeferredWork, performWithPriority: performWithPriority, + batchedUpdates: batchedUpdates, + syncUpdates: syncUpdates, }; }; diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index d352dd82c5f6..b1d985771c4a 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -22,6 +22,8 @@ type UpdateQueueNode = { export type UpdateQueue = UpdateQueueNode & { isReplace: boolean, isForced: boolean, + hasUpdate: boolean, + hasCallback: boolean, tail: UpdateQueueNode }; @@ -33,6 +35,8 @@ exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { next: null, isReplace: false, isForced: false, + hasUpdate: partialState != null, + hasCallback: false, tail: (null : any), }; queue.tail = queue; @@ -48,6 +52,7 @@ function addToQueue(queue : UpdateQueue, partialState : mixed) : UpdateQueue { }; queue.tail.next = node; queue.tail = node; + queue.hasUpdate = queue.hasUpdate || (partialState == null); return queue; } @@ -59,28 +64,34 @@ exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : addToQueue(queue, null); } queue.tail.callback = callback; + queue.hasCallback = true; return queue; }; exports.callCallbacks = function(queue : UpdateQueue, context : any) { let node : ?UpdateQueueNode = queue; while (node) { - if (node.callback && !node.callbackWasCalled) { + const callback = node.callback; + if (callback && !node.callbackWasCalled) { node.callbackWasCalled = true; - node.callback.call(context); + if (typeof context !== 'undefined') { + callback.call(context); + } else { + callback(); + } } node = node.next; } }; -exports.mergeUpdateQueue = function(queue : UpdateQueue, prevState : any, props : any) : any { +exports.mergeUpdateQueue = function(queue : UpdateQueue, instance : any, 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); + partialState = updateFn.call(instance, state, props); } else { partialState = node.partialState; } diff --git a/src/renderers/shared/fiber/ReactTypeOfSideEffect.js b/src/renderers/shared/fiber/ReactTypeOfSideEffect.js index 55a9cabc42e7..19daf44853ff 100644 --- a/src/renderers/shared/fiber/ReactTypeOfSideEffect.js +++ b/src/renderers/shared/fiber/ReactTypeOfSideEffect.js @@ -12,12 +12,17 @@ 'use strict'; -export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4; +export type TypeOfSideEffect = 0 | 1 | 2 | 3 | 4 | 8 | 9 | 10 | 11 | 12; module.exports = { - NoEffect: 0, - Placement: 1, - Update: 2, - PlacementAndUpdate: 3, - Deletion: 4, + NoEffect: 0, // 0b0000 + Placement: 1, // 0b0001 + Update: 2, // 0b0010 + PlacementAndUpdate: 3, // 0b0011 + Deletion: 4, // 0b0100 + Callback: 8, // 0b1000 + PlacementAndCallback: 9, // 0b1001 + UpdateAndCallback: 10, // 0b1010 + PlacementAndUpdateAndCallback: 11, // 0b1011 + DeletionAndCallback: 12, // 0b1100 }; diff --git a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js index ed775a969a59..5df5c10caf80 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncremental-test.js @@ -1386,4 +1386,83 @@ describe('ReactIncremental', () => { ]); }); + it('performs batched updates at the end of the batch', () => { + var ops = []; + var instance; + + class Foo extends React.Component { + state = { n: 0 }; + render() { + instance = this; + return
; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + ops = []; + + ReactNoop.syncUpdates(() => { + ReactNoop.batchedUpdates(() => { + instance.setState({ n: 1 }, () => ops.push('setState 1')); + instance.setState({ n: 2 }, () => ops.push('setState 2')); + ops.push('end batchedUpdates'); + }); + ops.push('end syncUpdates'); + }); + + // ReactNoop.flush() not needed because updates are synchronous + + expect(ops).toEqual([ + 'end batchedUpdates', + 'setState 1', + 'setState 2', + 'end syncUpdates', + ]); + expect(instance.state.n).toEqual(2); + }); + + it('can nest batchedUpdates', () => { + var ops = []; + var instance; + + class Foo extends React.Component { + state = { n: 0 }; + render() { + instance = this; + return
; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + ops = []; + + ReactNoop.syncUpdates(() => { + ReactNoop.batchedUpdates(() => { + instance.setState({ n: 1 }, () => ops.push('setState 1')); + instance.setState({ n: 2 }, () => ops.push('setState 2')); + ReactNoop.batchedUpdates(() => { + instance.setState({ n: 3 }, () => ops.push('setState 3')); + instance.setState({ n: 4 }, () => ops.push('setState 4')); + ops.push('end inner batchedUpdates'); + }); + ops.push('end outer batchedUpdates'); + }); + ops.push('end syncUpdates'); + }); + + // ReactNoop.flush() not needed because updates are synchronous + + expect(ops).toEqual([ + 'end inner batchedUpdates', + 'end outer batchedUpdates', + 'setState 1', + 'setState 2', + 'setState 3', + 'setState 4', + 'end syncUpdates', + ]); + expect(instance.state.n).toEqual(4); + }); }); diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js index 6722ccdf33dd..9e64dc43c130 100644 --- a/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalSideEffects-test.js @@ -860,6 +860,35 @@ describe('ReactIncrementalSideEffects', () => { expect(called).toBe(true); }); + it('calls setState callback even if component bails out', () => { + let instance; + class Foo extends React.Component { + constructor() { + super(); + instance = this; + this.state = { text: 'foo' }; + } + shouldComponentUpdate(nextProps, nextState) { + return this.state.text !== nextState.text; + } + render() { + return ; + } + } + + ReactNoop.render(); + ReactNoop.flush(); + expect(ReactNoop.getChildren()).toEqual([ + span('foo'), + ]); + let called = false; + instance.setState({}, () => { + called = true; + }); + ReactNoop.flush(); + expect(called).toBe(true); + }); + // TODO: Test that callbacks are not lost if an update is preempted. it('calls componentWillUnmount after a deletion, even if nested', () => { diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js index c758aa5d2e66..ac55f596fc76 100644 --- a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js +++ b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js @@ -11,6 +11,8 @@ 'use strict'; +var ReactDOMFeatureFlags = require('ReactDOMFeatureFlags'); + var React; var ReactDOM; @@ -148,7 +150,7 @@ describe('ReactCompositeComponent-state', () => { ReactDOM.unmountComponentAtNode(container); - expect(stateListener.mock.calls.join('\n')).toEqual([ + let expected = [ // there is no state when getInitialState() is called ['getInitialState', null], ['componentWillMount-start', 'red'], @@ -167,17 +169,44 @@ describe('ReactCompositeComponent-state', () => { // componentDidMount() called setState({color:'yellow'}), which is async. // The update doesn't happen until the next flush. ['componentDidMount-end', 'orange'], - ['shouldComponentUpdate-currentState', 'orange'], - ['shouldComponentUpdate-nextState', 'yellow'], - ['componentWillUpdate-currentState', 'orange'], - ['componentWillUpdate-nextState', 'yellow'], - ['render', 'yellow'], - ['componentDidUpdate-currentState', 'yellow'], - ['componentDidUpdate-prevState', 'orange'], - ['setState-sunrise', 'yellow'], - ['setState-orange', 'yellow'], - ['setState-yellow', 'yellow'], - ['initial-callback', 'yellow'], + ]; + + if (ReactDOMFeatureFlags.useFiber) { + // The setState callbacks in componentWillMount, and the initial callback + // passed to ReactDOM.render, should be flushed right after component + // did mount: + expected.push( + ['setState-sunrise', 'orange'], // 1 + ['setState-orange', 'orange'], // 2 + ['initial-callback', 'orange'], // 3 + ['shouldComponentUpdate-currentState', 'orange'], + ['shouldComponentUpdate-nextState', 'yellow'], + ['componentWillUpdate-currentState', 'orange'], + ['componentWillUpdate-nextState', 'yellow'], + ['render', 'yellow'], + ['componentDidUpdate-currentState', 'yellow'], + ['componentDidUpdate-prevState', 'orange'], + ['setState-yellow', 'yellow'], + ); + } else { + // There is a bug in the stack reconciler where those callbacks are + // enqueued, but aren't called until the next flush. + expected.push( + ['shouldComponentUpdate-currentState', 'orange'], + ['shouldComponentUpdate-nextState', 'yellow'], + ['componentWillUpdate-currentState', 'orange'], + ['componentWillUpdate-nextState', 'yellow'], + ['render', 'yellow'], + ['componentDidUpdate-currentState', 'yellow'], + ['componentDidUpdate-prevState', 'orange'], + ['setState-sunrise', 'yellow'], // 1 + ['setState-orange', 'yellow'], // 2 + ['setState-yellow', 'yellow'], + ['initial-callback', 'yellow'] // 3 + ); + } + + expected.push( ['componentWillReceiveProps-start', 'yellow'], // setState({color:'green'}) only enqueues a pending state. ['componentWillReceiveProps-end', 'yellow'], @@ -213,7 +242,9 @@ describe('ReactCompositeComponent-state', () => { // unmountComponent() // state is available within `componentWillUnmount()` ['componentWillUnmount', 'blue'], - ].join('\n')); + ); + + expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n')); }); it('should batch unmounts', () => {