From 3eb27b3cc008f0fe7dce80a24522659d79d08b7f Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 10:58:40 -0700 Subject: [PATCH 01/14] Ensure first iteration of performAnimationWork loop has right priority I don't think this actually changes any behavior because of the way findNextUnitOfWork works, but I think this is easier to follow. --- src/renderers/shared/fiber/ReactFiberScheduler.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 4c7b0c515422a..6dfc79f3c4910 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -417,17 +417,16 @@ 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); } } From 1671f054ba838f632976ea3d0cc0c51c7c3a994c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 11:12:10 -0700 Subject: [PATCH 02/14] Synchronous work --- .../shared/fiber/ReactFiberScheduler.js | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 6dfc79f3c4910..00c1fb69d1d54 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -365,20 +365,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(performDeferredWorkUnsafe, deadline); } function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { @@ -431,20 +418,7 @@ module.exports = function(config : HostConfig) { } 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(performAnimationWorkUnsafe); } function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { @@ -497,6 +471,46 @@ module.exports = function(config : HostConfig) { return root; } + function performSynchronousWorkUnsafe() { + // Perform work now + nextUnitOfWork = findNextUnitOfWork(); + while (nextUnitOfWork && + nextPriorityLevel === SynchronousPriority) { + nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); + // If there's no nextUnitForWork, we don't need to search for more + // because it shouldn't be possible to schedule sync work without + // immediately performing it + } + if (nextUnitOfWork) { + if (nextPriorityLevel > AnimationPriority) { + scheduleDeferredCallback(performDeferredWork); + return; + } + scheduleAnimationCallback(performAnimationWork); + } + } + + function performSynchronousWork() { + performAndHandleErrors(performSynchronousWorkUnsafe); + } + + function performAndHandleErrors(fn: (a: A) => void, a: A) { + try { + fn(a); + } 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; @@ -571,7 +585,8 @@ module.exports = function(config : HostConfig) { function scheduleWork(root : FiberRoot) { if (defaultPriority === SynchronousPriority) { - throw new Error('Not implemented yet'); + root.current.pendingWorkPriority = SynchronousPriority; + performSynchronousWork(); } if (defaultPriority === NoWork) { From 0e73a370f2859004758a9d66523d265696356c59 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 11:26:03 -0700 Subject: [PATCH 03/14] Updates should use the same priority context as top-level render A bit of restructuring so that setState uses whatever the current priority context is, like top-level render already does. Renamed defaultPriority to priorityContext, and added a new variable called defaultPriorityContext. Having two separate variables allows the default to be changed without interfering with the current context. --- .../shared/fiber/ReactFiberBeginWork.js | 2 +- .../shared/fiber/ReactFiberClassComponent.js | 13 +++-- .../shared/fiber/ReactFiberScheduler.js | 52 ++++++++++++++----- 3 files changed, 45 insertions(+), 22 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index bcc8958e54222..d8b41f77378cf 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -48,7 +48,7 @@ var ReactFiberClassComponent = require('ReactFiberClassComponent'); module.exports = function( config : HostConfig, - scheduleUpdate : (fiber: Fiber, priorityLevel : PriorityLevel) => void + scheduleUpdate : (fiber: Fiber, priorityLevel : ?PriorityLevel) => void ) { const { diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 2211dead1876f..d775860d248ce 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -16,7 +16,6 @@ import type { Fiber } from 'ReactFiber'; import type { PriorityLevel } from 'ReactPriorityLevel'; import type { UpdateQueue } from 'ReactFiberUpdateQueue'; -var { LowPriority } = require('ReactPriorityLevel'); var { createUpdateQueue, addToQueue, @@ -27,16 +26,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, priorityLevel : ?PriorityLevel) => 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 +46,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); diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 00c1fb69d1d54..ced9a48be12bb 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -63,8 +63,10 @@ module.exports = function(config : HostConfig) { const scheduleAnimationCallback = config.scheduleAnimationCallback; const scheduleDeferredCallback = config.scheduleDeferredCallback; - // The default priority to use for updates. - let defaultPriority : PriorityLevel = LowPriority; + // The priority level to use when scheduling an update. + let priorityContext : (PriorityLevel | null) = null; + // The priority level to use if there is no priority context. + let defaultPriorityContext : PriorityLevel = LowPriority; // The next work in progress fiber that we're currently working on. let nextUnitOfWork : ?Fiber = null; @@ -477,7 +479,7 @@ module.exports = function(config : HostConfig) { while (nextUnitOfWork && nextPriorityLevel === SynchronousPriority) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); - // If there's no nextUnitForWork, we don't need to search for more + // If there's no nextUnitOfWork, we don't need to search for more // because it shouldn't be possible to schedule sync work without // immediately performing it } @@ -583,23 +585,45 @@ module.exports = function(config : HostConfig) { } } - function scheduleWork(root : FiberRoot) { - if (defaultPriority === SynchronousPriority) { + function scheduleWork(root : FiberRoot, priorityLevel : ?PriorityLevel) { + if (priorityLevel == null) { + priorityLevel = priorityContext !== null ? + priorityContext : + defaultPriorityContext; + } + + if (priorityLevel === SynchronousPriority) { root.current.pendingWorkPriority = SynchronousPriority; performSynchronousWork(); } - if (defaultPriority === NoWork) { + if (priorityLevel === NoWork) { return; } - if (defaultPriority > AnimationPriority) { - scheduleDeferredWork(root, defaultPriority); + if (priorityLevel > AnimationPriority) { + scheduleDeferredWork(root, priorityLevel); return; } - scheduleAnimationWork(root, defaultPriority); + scheduleAnimationWork(root, priorityLevel); } - function scheduleUpdate(fiber: Fiber, priorityLevel : PriorityLevel): void { + function scheduleUpdate(fiber: Fiber, priorityLevel : ?PriorityLevel): void { + // Use priority context if no priority is provided + if (priorityLevel == null) { + priorityLevel = priorityContext !== null ? + priorityContext : + defaultPriorityContext; + } + + // Don't bother bubbling the priority to the root if it is synchronous. Just + // perform it now. + if (priorityLevel === SynchronousPriority) { + fiber.pendingWorkPriority = SynchronousPriority; + nextUnitOfWork = fiber; + performSynchronousWork(); + return; + } + while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { @@ -614,7 +638,7 @@ module.exports = function(config : HostConfig) { if (!fiber.return) { if (fiber.tag === HostContainer) { const root : FiberRoot = (fiber.stateNode : any); - scheduleDeferredWork(root, priorityLevel); + scheduleWork(root, priorityLevel); return; } else { throw new Error('Invalid root'); @@ -625,12 +649,12 @@ 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; } } From 935b4dba313f471cd0bd5eb3499583aafcdc5fa1 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 11:44:52 -0700 Subject: [PATCH 04/14] Add config to enable sync scheduling by default I'll turn this on in ReactDOMFiber once I figure out batchedUpdates. --- src/renderers/shared/fiber/ReactFiberReconciler.js | 3 ++- src/renderers/shared/fiber/ReactFiberScheduler.js | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index d5995a3c65b01..d86c4946d4ed0 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -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; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index ced9a48be12bb..ad28d0991123e 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -62,11 +62,14 @@ 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 | null) = null; // The priority level to use if there is no priority context. - let defaultPriorityContext : PriorityLevel = LowPriority; + let defaultPriorityContext : PriorityLevel = useSyncScheduling ? + SynchronousPriority : + LowPriority; // The next work in progress fiber that we're currently working on. let nextUnitOfWork : ?Fiber = null; From ad2ffb9cdbc614ae36d7e5db010beca4815e49f2 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 13:26:25 -0700 Subject: [PATCH 05/14] Batch nested updates when in sync mode Almost working... --- src/renderers/dom/fiber/ReactDOMFiber.js | 2 + .../shared/fiber/ReactFiberClassComponent.js | 6 +- .../shared/fiber/ReactFiberScheduler.js | 57 +++++++++++++------ .../shared/fiber/ReactFiberUpdateQueue.js | 4 +- 4 files changed, 46 insertions(+), 23 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 8294883db55bc..ca3b1f703b309 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; diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index d775860d248ce..7652ce1464945 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -130,7 +130,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?Prior // process them now. const updateQueue = workInProgress.updateQueue; if (updateQueue) { - instance.state = mergeUpdateQueue(updateQueue, state, props); + instance.state = mergeUpdateQueue(updateQueue, instance, state, props); } } } @@ -174,7 +174,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?Prior // process them now. const newUpdateQueue = workInProgress.updateQueue; if (newUpdateQueue) { - newInstance.state = mergeUpdateQueue(newUpdateQueue, newState, newProps); + newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps); } } return true; @@ -211,7 +211,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?Prior // TODO: Previous state can be null. let newState; if (updateQueue) { - newState = mergeUpdateQueue(updateQueue, previousState, newProps); + newState = mergeUpdateQueue(updateQueue, instance, previousState, newProps); } else { newState = previousState; } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index ad28d0991123e..24bab83e3c8bb 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -71,6 +71,9 @@ module.exports = function(config : HostConfig) { SynchronousPriority : 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; let nextPriorityLevel : PriorityLevel = NoWork; @@ -482,9 +485,10 @@ module.exports = function(config : HostConfig) { while (nextUnitOfWork && nextPriorityLevel === SynchronousPriority) { nextUnitOfWork = performUnitOfWork(nextUnitOfWork, false); - // If there's no nextUnitOfWork, we don't need to search for more - // because it shouldn't be possible to schedule sync work without - // immediately performing it + + if (!nextUnitOfWork && shouldBatchUpdates) { + nextUnitOfWork = findNextUnitOfWork(); + } } if (nextUnitOfWork) { if (nextPriorityLevel > AnimationPriority) { @@ -496,7 +500,35 @@ module.exports = function(config : HostConfig) { } function performSynchronousWork() { + if (useSyncScheduling) { + // Start batching updates + shouldBatchUpdates = true; + } performAndHandleErrors(performSynchronousWorkUnsafe); + shouldBatchUpdates = false; + } + + 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) { + performSynchronousWork(); + } + } } function performAndHandleErrors(fn: (a: A) => void, a: A) { @@ -552,10 +584,9 @@ 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 !== null ? + priorityContext : + defaultPriorityContext; const root = scheduleErrorBoundaryWork(boundary, priority); // This should use findNextUnitOfWork() when synchronous scheduling is implemented. let fiber = cloneFiber(root.current, priority); @@ -596,8 +627,7 @@ module.exports = function(config : HostConfig) { } if (priorityLevel === SynchronousPriority) { - root.current.pendingWorkPriority = SynchronousPriority; - performSynchronousWork(); + scheduleSynchronousWork(root); } if (priorityLevel === NoWork) { @@ -618,15 +648,6 @@ module.exports = function(config : HostConfig) { defaultPriorityContext; } - // Don't bother bubbling the priority to the root if it is synchronous. Just - // perform it now. - if (priorityLevel === SynchronousPriority) { - fiber.pendingWorkPriority = SynchronousPriority; - nextUnitOfWork = fiber; - performSynchronousWork(); - return; - } - while (true) { if (fiber.pendingWorkPriority === NoWork || fiber.pendingWorkPriority >= priorityLevel) { diff --git a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js index d352dd82c5f60..8e27e739811d7 100644 --- a/src/renderers/shared/fiber/ReactFiberUpdateQueue.js +++ b/src/renderers/shared/fiber/ReactFiberUpdateQueue.js @@ -73,14 +73,14 @@ exports.callCallbacks = function(queue : UpdateQueue, context : any) { } }; -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; } From c16ca460bcdc64afd85a790dbbbc352bd6fda0f4 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 22:53:37 -0700 Subject: [PATCH 06/14] Enqueue update and callback simultaneously Without this fix, in non-batched mode, the update is scheduled first and synchronously flushed before the callback is added to the queue. The callback isn't called until the next flush. --- src/isomorphic/classic/class/ReactClass.js | 5 + src/isomorphic/modern/class/ReactComponent.js | 11 + .../shared/fiber/ReactFiberClassComponent.js | 27 ++- .../ReactCompositeComponentState-test.js | 208 ++++++++++++------ 4 files changed, 171 insertions(+), 80 deletions(-) diff --git a/src/isomorphic/classic/class/ReactClass.js b/src/isomorphic/classic/class/ReactClass.js index 475f08d7d81b2..f0a59650062b8 100644 --- a/src/isomorphic/classic/class/ReactClass.js +++ b/src/isomorphic/classic/class/ReactClass.js @@ -728,6 +728,11 @@ var ReactClassMixin = { * type signature and the only use case for this, is to avoid that. */ replaceState: function(newState, callback) { + if (this.updater.isFiberUpdater) { + this.updater.enqueueReplaceState(this, newState, callback); + return; + } + this.updater.enqueueReplaceState(this, newState); if (callback) { this.updater.enqueueCallback(this, callback, 'replaceState'); diff --git a/src/isomorphic/modern/class/ReactComponent.js b/src/isomorphic/modern/class/ReactComponent.js index b93da63e94cf9..1eac5e786dee5 100644 --- a/src/isomorphic/modern/class/ReactComponent.js +++ b/src/isomorphic/modern/class/ReactComponent.js @@ -65,6 +65,12 @@ ReactComponent.prototype.setState = function(partialState, callback) { 'setState(...): takes an object of state variables to update or a ' + 'function which returns an object of state variables.' ); + + if (this.updater.isFiberUpdater) { + this.updater.enqueueSetState(this, partialState, callback); + return; + } + this.updater.enqueueSetState(this, partialState); if (callback) { this.updater.enqueueCallback(this, callback, 'setState'); @@ -86,6 +92,11 @@ ReactComponent.prototype.setState = function(partialState, callback) { * @protected */ ReactComponent.prototype.forceUpdate = function(callback) { + if (this.updater.isFiberUpdater) { + this.updater.enqueueForceUpdate(this, callback); + return; + } + this.updater.enqueueForceUpdate(this); if (callback) { this.updater.enqueueCallback(this, callback, 'forceUpdate'); diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 7652ce1464945..ac07fc852cdf6 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -41,36 +41,35 @@ module.exports = function(scheduleUpdate : (fiber: Fiber, priorityLevel : ?Prior // Class component state updater const updater = { isMounted, - enqueueSetState(instance, partialState) { + enqueueSetState(instance, partialState, callback) { const fiber = ReactInstanceMap.get(instance); const updateQueue = fiber.updateQueue ? addToQueue(fiber.updateQueue, partialState) : createUpdateQueue(partialState); + if (callback) { + addCallbackToQueue(updateQueue, callback); + } scheduleUpdateQueue(fiber, updateQueue); }, - enqueueReplaceState(instance, state) { + enqueueReplaceState(instance, state, callback) { const fiber = ReactInstanceMap.get(instance); const updateQueue = createUpdateQueue(state); updateQueue.isReplace = true; + if (callback) { + addCallbackToQueue(updateQueue, callback); + } scheduleUpdateQueue(fiber, updateQueue); }, - enqueueForceUpdate(instance) { + enqueueForceUpdate(instance, callback) { const fiber = ReactInstanceMap.get(instance); const updateQueue = fiber.updateQueue || createUpdateQueue(null); updateQueue.isForced = true; - scheduleUpdateQueue(fiber, updateQueue); - }, - 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; + if (callback) { + addCallbackToQueue(updateQueue, callback); } + scheduleUpdateQueue(fiber, updateQueue); }, + isFiberUpdater: true, }; function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState) { diff --git a/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js b/src/renderers/shared/stack/reconciler/__tests__/ReactCompositeComponentState-test.js index c758aa5d2e665..5c6c34879d6b3 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,72 +150,146 @@ describe('ReactCompositeComponent-state', () => { ReactDOM.unmountComponentAtNode(container); - expect(stateListener.mock.calls.join('\n')).toEqual([ - // there is no state when getInitialState() is called - ['getInitialState', null], - ['componentWillMount-start', 'red'], - // setState()'s only enqueue pending states. - ['componentWillMount-after-sunrise', 'red'], - ['componentWillMount-end', 'red'], - // pending state queue is processed - ['before-setState-sunrise', 'red'], - ['after-setState-sunrise', 'sunrise'], - ['after-setState-orange', 'orange'], - // pending state has been applied - ['render', 'orange'], - ['componentDidMount-start', 'orange'], - // setState-sunrise and setState-orange should be called here, - // after the bug in #1740 - // 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'], - ['componentWillReceiveProps-start', 'yellow'], - // setState({color:'green'}) only enqueues a pending state. - ['componentWillReceiveProps-end', 'yellow'], - // pending state queue is processed - // before-setState-receiveProps never called, due to replaceState. - ['before-setState-again-receiveProps', undefined], - ['after-setState-receiveProps', 'green'], - ['shouldComponentUpdate-currentState', 'yellow'], - ['shouldComponentUpdate-nextState', 'green'], - ['componentWillUpdate-currentState', 'yellow'], - ['componentWillUpdate-nextState', 'green'], - ['render', 'green'], - ['componentDidUpdate-currentState', 'green'], - ['componentDidUpdate-prevState', 'yellow'], - ['setState-receiveProps', 'green'], - ['setProps', 'green'], - // setFavoriteColor('blue') - ['shouldComponentUpdate-currentState', 'green'], - ['shouldComponentUpdate-nextState', 'blue'], - ['componentWillUpdate-currentState', 'green'], - ['componentWillUpdate-nextState', 'blue'], - ['render', 'blue'], - ['componentDidUpdate-currentState', 'blue'], - ['componentDidUpdate-prevState', 'green'], - ['setFavoriteColor', 'blue'], - // forceUpdate() - ['componentWillUpdate-currentState', 'blue'], - ['componentWillUpdate-nextState', 'blue'], - ['render', 'blue'], - ['componentDidUpdate-currentState', 'blue'], - ['componentDidUpdate-prevState', 'blue'], - ['forceUpdate', 'blue'], - // unmountComponent() - // state is available within `componentWillUnmount()` - ['componentWillUnmount', 'blue'], - ].join('\n')); + let expected; + if (ReactDOMFeatureFlags.useFiber) { + expected = [ + // there is no state when getInitialState() is called + ['getInitialState', null], + ['componentWillMount-start', 'red'], + // setState()'s only enqueue pending states. + ['componentWillMount-after-sunrise', 'red'], + ['componentWillMount-end', 'red'], + // pending state queue is processed + ['before-setState-sunrise', 'red'], + ['after-setState-sunrise', 'sunrise'], + ['after-setState-orange', 'orange'], + // pending state has been applied + ['render', 'orange'], + ['componentDidMount-start', 'orange'], + // setState-sunrise and setState-orange should be called here, + // after the bug in #1740 + // componentDidMount() called setState({color:'yellow'}), which is async. + // The update doesn't happen until the next flush. + ['componentDidMount-end', 'orange'], + ['setState-sunrise', 'orange'], + ['setState-orange', 'orange'], + ['initial-callback', 'orange'], + ['shouldComponentUpdate-currentState', 'orange'], + ['shouldComponentUpdate-nextState', 'yellow'], + ['componentWillUpdate-currentState', 'orange'], + ['componentWillUpdate-nextState', 'yellow'], + ['render', 'yellow'], + ['componentDidUpdate-currentState', 'yellow'], + ['componentDidUpdate-prevState', 'orange'], + ['setState-yellow', 'yellow'], + ['componentWillReceiveProps-start', 'yellow'], + // setState({color:'green'}) only enqueues a pending state. + ['componentWillReceiveProps-end', 'yellow'], + // pending state queue is processed + // before-setState-receiveProps never called, due to replaceState. + ['before-setState-again-receiveProps', undefined], + ['after-setState-receiveProps', 'green'], + ['shouldComponentUpdate-currentState', 'yellow'], + ['shouldComponentUpdate-nextState', 'green'], + ['componentWillUpdate-currentState', 'yellow'], + ['componentWillUpdate-nextState', 'green'], + ['render', 'green'], + ['componentDidUpdate-currentState', 'green'], + ['componentDidUpdate-prevState', 'yellow'], + ['setState-receiveProps', 'green'], + ['setProps', 'green'], + // setFavoriteColor('blue') + ['shouldComponentUpdate-currentState', 'green'], + ['shouldComponentUpdate-nextState', 'blue'], + ['componentWillUpdate-currentState', 'green'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'green'], + ['setFavoriteColor', 'blue'], + // forceUpdate() + ['componentWillUpdate-currentState', 'blue'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'blue'], + ['forceUpdate', 'blue'], + // unmountComponent() + // state is available within `componentWillUnmount()` + ['componentWillUnmount', 'blue'], + ]; + } else { + // There's a bug in the stack reconciler where setState callbacks inside + // componentWillMount aren't flushed properly + expected = [ + // there is no state when getInitialState() is called + ['getInitialState', null], + ['componentWillMount-start', 'red'], + // setState()'s only enqueue pending states. + ['componentWillMount-after-sunrise', 'red'], + ['componentWillMount-end', 'red'], + // pending state queue is processed + ['before-setState-sunrise', 'red'], + ['after-setState-sunrise', 'sunrise'], + ['after-setState-orange', 'orange'], + // pending state has been applied + ['render', 'orange'], + ['componentDidMount-start', 'orange'], + // setState-sunrise and setState-orange should be called here, + // after the bug in #1740 + // 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'], + ['componentWillReceiveProps-start', 'yellow'], + // setState({color:'green'}) only enqueues a pending state. + ['componentWillReceiveProps-end', 'yellow'], + // pending state queue is processed + // before-setState-receiveProps never called, due to replaceState. + ['before-setState-again-receiveProps', undefined], + ['after-setState-receiveProps', 'green'], + ['shouldComponentUpdate-currentState', 'yellow'], + ['shouldComponentUpdate-nextState', 'green'], + ['componentWillUpdate-currentState', 'yellow'], + ['componentWillUpdate-nextState', 'green'], + ['render', 'green'], + ['componentDidUpdate-currentState', 'green'], + ['componentDidUpdate-prevState', 'yellow'], + ['setState-receiveProps', 'green'], + ['setProps', 'green'], + // setFavoriteColor('blue') + ['shouldComponentUpdate-currentState', 'green'], + ['shouldComponentUpdate-nextState', 'blue'], + ['componentWillUpdate-currentState', 'green'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'green'], + ['setFavoriteColor', 'blue'], + // forceUpdate() + ['componentWillUpdate-currentState', 'blue'], + ['componentWillUpdate-nextState', 'blue'], + ['render', 'blue'], + ['componentDidUpdate-currentState', 'blue'], + ['componentDidUpdate-prevState', 'blue'], + ['forceUpdate', 'blue'], + // unmountComponent() + // state is available within `componentWillUnmount()` + ['componentWillUnmount', 'blue'], + ]; + } + + expect(stateListener.mock.calls.join('\n')).toEqual(expected.join('\n')); }); it('should batch unmounts', () => { From 8996c9c498a5bab632ffbf0007982f76d4cc1294 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 30 Oct 2016 22:26:57 -0700 Subject: [PATCH 07/14] unstable_batchedUpdates Implements batchedUpdates and exposes as unstable_batchedUpdates. All nested synchronous updates are automatically batched. --- src/renderers/dom/fiber/ReactDOMFiber.js | 4 ++++ .../shared/fiber/ReactFiberReconciler.js | 9 +++++++- .../shared/fiber/ReactFiberScheduler.js | 22 ++++++++++++++----- 3 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index ca3b1f703b309..6d59e5aed7821 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -165,6 +165,10 @@ var ReactDOM = { return DOMRenderer.findHostInstance(component); }, + unstable_batchedUpdates(fn : Function) : void { + DOMRenderer.batchedUpdates(fn); + }, + }; module.exports = ReactDOM; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index d86c4946d4ed0..f14559dfa806b 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -68,6 +68,7 @@ export type Reconciler = { updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, performWithPriority(priorityLevel : PriorityLevel, fn : Function) : void, + batchedUpdates(fn: Function) : void, // Used to extract the return value from the initial render. Legacy API. getPublicRootInstance(container : OpaqueNode) : (ReactComponent | TI | I | null), @@ -78,7 +79,11 @@ export type Reconciler = { module.exports = function(config : HostConfig) : Reconciler { - var { scheduleWork, performWithPriority } = ReactFiberScheduler(config); + var { + scheduleWork, + performWithPriority, + batchedUpdates, + } = ReactFiberScheduler(config); return { @@ -142,6 +147,8 @@ module.exports = function(config : HostConfig) : performWithPriority, + batchedUpdates, + 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 24bab83e3c8bb..f6700bacaeadd 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -500,12 +500,10 @@ module.exports = function(config : HostConfig) { } function performSynchronousWork() { - if (useSyncScheduling) { - // Start batching updates - shouldBatchUpdates = true; - } - performAndHandleErrors(performSynchronousWorkUnsafe); - shouldBatchUpdates = false; + // All nested updates are batched + batchedUpdates(() => { + performAndHandleErrors(performSynchronousWorkUnsafe); + }); } function scheduleSynchronousWork(root : FiberRoot) { @@ -526,6 +524,7 @@ module.exports = function(config : HostConfig) { lastScheduledRoot = root; if (!shouldBatchUpdates) { + // Unless in batched mode, perform work immediately performSynchronousWork(); } } @@ -682,9 +681,20 @@ module.exports = function(config : HostConfig) { } } + function batchedUpdates(fn: Function) { + const prev = shouldBatchUpdates; + shouldBatchUpdates = true; + try { + fn(); + } finally { + shouldBatchUpdates = prev; + } + } + return { scheduleWork: scheduleWork, scheduleDeferredWork: scheduleDeferredWork, performWithPriority: performWithPriority, + batchedUpdates: batchedUpdates, }; }; From b44da09c7c98ca50581d94c449553e0c0726945a Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 30 Oct 2016 22:34:55 -0700 Subject: [PATCH 08/14] Don't need defaultPriorityContext Turns out this isn't necessary. Simpler to keep it as one field. --- .../shared/fiber/ReactFiberScheduler.js | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index f6700bacaeadd..0369677c6e4d3 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -65,9 +65,7 @@ module.exports = function(config : HostConfig) { const useSyncScheduling = config.useSyncScheduling; // The priority level to use when scheduling an update. - let priorityContext : (PriorityLevel | null) = null; - // The priority level to use if there is no priority context. - let defaultPriorityContext : PriorityLevel = useSyncScheduling ? + let priorityContext : PriorityLevel = useSyncScheduling ? SynchronousPriority : LowPriority; @@ -583,9 +581,7 @@ module.exports = function(config : HostConfig) { // We will process an update caused by each error boundary synchronously. affectedBoundaries.forEach(boundary => { - const priority = priorityContext !== null ? - priorityContext : - defaultPriorityContext; + const priority = priorityContext; const root = scheduleErrorBoundaryWork(boundary, priority); // This should use findNextUnitOfWork() when synchronous scheduling is implemented. let fiber = cloneFiber(root.current, priority); @@ -620,9 +616,7 @@ module.exports = function(config : HostConfig) { function scheduleWork(root : FiberRoot, priorityLevel : ?PriorityLevel) { if (priorityLevel == null) { - priorityLevel = priorityContext !== null ? - priorityContext : - defaultPriorityContext; + priorityLevel = priorityContext; } if (priorityLevel === SynchronousPriority) { @@ -642,9 +636,7 @@ module.exports = function(config : HostConfig) { function scheduleUpdate(fiber: Fiber, priorityLevel : ?PriorityLevel): void { // Use priority context if no priority is provided if (priorityLevel == null) { - priorityLevel = priorityContext !== null ? - priorityContext : - defaultPriorityContext; + priorityLevel = priorityContext; } while (true) { From 1c596eaf6758a24ddc753f6b8cec5915cd455f8c Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Sun, 30 Oct 2016 22:44:52 -0700 Subject: [PATCH 09/14] unstable_batchedUpdates should return value of callback --- src/renderers/dom/fiber/ReactDOMFiber.js | 4 ++-- src/renderers/shared/fiber/ReactFiberReconciler.js | 5 ++++- src/renderers/shared/fiber/ReactFiberScheduler.js | 4 ++-- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/renderers/dom/fiber/ReactDOMFiber.js b/src/renderers/dom/fiber/ReactDOMFiber.js index 6d59e5aed7821..08fd6d7c59df8 100644 --- a/src/renderers/dom/fiber/ReactDOMFiber.js +++ b/src/renderers/dom/fiber/ReactDOMFiber.js @@ -165,8 +165,8 @@ var ReactDOM = { return DOMRenderer.findHostInstance(component); }, - unstable_batchedUpdates(fn : Function) : void { - DOMRenderer.batchedUpdates(fn); + unstable_batchedUpdates(fn : () => A) : A { + return DOMRenderer.batchedUpdates(fn); }, }; diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index f14559dfa806b..7708e04c590cc 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -68,7 +68,10 @@ export type Reconciler = { updateContainer(element : ReactElement, container : OpaqueNode) : void, unmountContainer(container : OpaqueNode) : void, performWithPriority(priorityLevel : PriorityLevel, fn : Function) : void, - batchedUpdates(fn: Function) : void, + /* eslint-disable no-undef */ + // FIXME: ESLint complains about type parameter + batchedUpdates(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), diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 0369677c6e4d3..f2c41c872ad81 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -673,11 +673,11 @@ module.exports = function(config : HostConfig) { } } - function batchedUpdates(fn: Function) { + function batchedUpdates(fn : () => A) : A { const prev = shouldBatchUpdates; shouldBatchUpdates = true; try { - fn(); + return fn(); } finally { shouldBatchUpdates = prev; } From 80bb12425d5af18f1298270cffbb9decb9359222 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 31 Oct 2016 09:59:48 -0700 Subject: [PATCH 10/14] At the end of a batch, perform any sync work that was scheduled --- src/renderers/shared/fiber/ReactFiberScheduler.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index f2c41c872ad81..7e4dc6e439458 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -498,10 +498,14 @@ module.exports = function(config : HostConfig) { } function performSynchronousWork() { + const prev = shouldBatchUpdates; + shouldBatchUpdates = true; // All nested updates are batched - batchedUpdates(() => { + try { performAndHandleErrors(performSynchronousWorkUnsafe); - }); + } finally { + shouldBatchUpdates = prev; + } } function scheduleSynchronousWork(root : FiberRoot) { @@ -680,6 +684,10 @@ module.exports = function(config : HostConfig) { return fn(); } finally { shouldBatchUpdates = prev; + // If we've exited the batch, perform any scheduled sync work + if (!shouldBatchUpdates) { + performSynchronousWork(); + } } } From e20bafad30926cff4acb29803997c2e437c7a1f0 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Mon, 31 Oct 2016 11:18:22 -0700 Subject: [PATCH 11/14] Remove first-class function Instead we'll branch on the priority level, like in scheduleWork. --- .../shared/fiber/ReactFiberReconciler.js | 2 +- .../shared/fiber/ReactFiberScheduler.js | 25 ++++++++++++++----- 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 7708e04c590cc..fd7fc1b586225 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 }; diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 7e4dc6e439458..dc00975047c40 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'); @@ -371,7 +371,7 @@ module.exports = function(config : HostConfig) { } function performDeferredWork(deadline) { - performAndHandleErrors(performDeferredWorkUnsafe, deadline); + performAndHandleErrors(LowPriority, deadline); } function scheduleDeferredWork(root : FiberRoot, priority : PriorityLevel) { @@ -424,7 +424,7 @@ module.exports = function(config : HostConfig) { } function performAnimationWork() { - performAndHandleErrors(performAnimationWorkUnsafe); + performAndHandleErrors(AnimationPriority); } function scheduleAnimationWork(root: FiberRoot, priorityLevel : PriorityLevel) { @@ -502,7 +502,7 @@ module.exports = function(config : HostConfig) { shouldBatchUpdates = true; // All nested updates are batched try { - performAndHandleErrors(performSynchronousWorkUnsafe); + performAndHandleErrors(SynchronousPriority); } finally { shouldBatchUpdates = prev; } @@ -532,9 +532,22 @@ module.exports = function(config : HostConfig) { } } - function performAndHandleErrors(fn: (a: A) => void, a: A) { + function performAndHandleErrors(priorityLevel : PriorityLevel, deadline : ?Deadline) { + // The exact priority level doesn't matter, so long as it's in range of the + // work (sync, animation, deferred) being performed. try { - fn(a); + if (priorityLevel === SynchronousPriority) { + performSynchronousWorkUnsafe(); + } + if (priorityLevel > AnimationPriority) { + if (!deadline) { + throw new Error('No deadline'); + } else { + performDeferredWorkUnsafe(deadline); + } + return; + } + performAnimationWorkUnsafe(); } catch (error) { const failedUnitOfWork = nextUnitOfWork; // Reset because it points to the error boundary: From 51cc0a40d217a8c8a8e3819323bd3e939f293f82 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 31 Oct 2016 17:56:51 +0000 Subject: [PATCH 12/14] Remove recursion from error handling This removes the only potentially recursive call in Fiber codebase by moving the error handling outside commitAllWork(), and thus, outside performUnitOfWork(). --- .../shared/fiber/ReactFiberScheduler.js | 23 +++++++++++-------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index dc00975047c40..b484edefa1627 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -76,11 +76,19 @@ module.exports = function(config : HostConfig) { let nextUnitOfWork : ?Fiber = null; let nextPriorityLevel : PriorityLevel = NoWork; + // The next errors we need to handle before the next item of work. + let nextTrappedErrors : Array | null = null; + // Linked list of roots with scheduled work on them. let nextScheduledRoot : ?FiberRoot = null; let lastScheduledRoot : ?FiberRoot = null; function findNextUnitOfWork() { + if (nextTrappedErrors) { + // While we have errors, we can't perform any normal work. + return null; + } + // Clear out roots with no more work on them. while (nextScheduledRoot && nextScheduledRoot.current.pendingWorkPriority === NoWork) { nextScheduledRoot.isScheduled = false; @@ -210,10 +218,7 @@ module.exports = function(config : HostConfig) { } } - // Now that the tree has been committed, we can handle errors. - if (allTrappedErrors) { - handleErrors(allTrappedErrors); - } + nextTrappedErrors = allTrappedErrors; } function resetWorkPriority(workInProgress : Fiber) { @@ -558,12 +563,14 @@ module.exports = function(config : HostConfig) { throw error; } const trappedError = trapError(failedUnitOfWork, error); - handleErrors([trappedError]); + nextTrappedErrors = [trappedError]; + } + if (nextTrappedErrors) { + handleErrors(); } } - function handleErrors(initialTrappedErrors : Array) : void { - let nextTrappedErrors = initialTrappedErrors; + function handleErrors() : void { let firstUncaughtError = null; // In each phase, we will attempt to pass errors to boundaries and re-render them. @@ -604,8 +611,6 @@ module.exports = function(config : HostConfig) { let fiber = cloneFiber(root.current, priority); try { while (fiber) { - // TODO: this is the only place where we recurse and it's unfortunate. - // (This may potentially get us into handleErrors() again.) fiber = performUnitOfWork(fiber, true); } } catch (nextError) { From 51ad4f12adf4e8b5076d276ff8750916374fb6cc Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 31 Oct 2016 21:28:07 +0000 Subject: [PATCH 13/14] Unschedule roots with uncaught errors --- .../shared/fiber/ReactFiberErrorBoundary.js | 18 +++++ .../shared/fiber/ReactFiberScheduler.js | 77 +++++++++++-------- 2 files changed, 63 insertions(+), 32 deletions(-) diff --git a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js index 7268882155963..f064c7f3f6423 100644 --- a/src/renderers/shared/fiber/ReactFiberErrorBoundary.js +++ b/src/renderers/shared/fiber/ReactFiberErrorBoundary.js @@ -13,14 +13,17 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; +import type { FiberRoot } from 'ReactFiberRoot'; var { ClassComponent, + HostContainer, } = require('ReactTypeOfWork'); export type TrappedError = { boundary: Fiber | null, error: any, + root: FiberRoot, }; function findClosestErrorBoundary(fiber : Fiber): Fiber | null { @@ -37,9 +40,24 @@ function findClosestErrorBoundary(fiber : Fiber): Fiber | null { return null; } +function findRoot(fiber : Fiber) : FiberRoot { + while (fiber) { + if (!fiber.return) { + if (fiber.tag === HostContainer) { + return ((fiber.stateNode : any) : FiberRoot); + } else { + throw new Error('Invalid root'); + } + } + fiber = fiber.return; + } + throw new Error('Could not find a root.'); +} + function trapError(fiber : Fiber, error : any) : TrappedError { return { boundary: findClosestErrorBoundary(fiber), + root: findRoot(fiber), error, }; } diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index b484edefa1627..e4a04eb478203 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -456,30 +456,15 @@ module.exports = function(config : HostConfig) { } } - function scheduleErrorBoundaryWork(boundary : Fiber, priority) : FiberRoot { - let root = null; + function scheduleErrorBoundaryWork(boundary : Fiber, priority) { let fiber = boundary; while (fiber) { fiber.pendingWorkPriority = priority; if (fiber.alternate) { fiber.alternate.pendingWorkPriority = priority; } - if (!fiber.return) { - if (fiber.tag === HostContainer) { - // We found the root. - // Remember it so we can update it. - root = ((fiber.stateNode : any) : FiberRoot); - break; - } else { - throw new Error('Invalid root'); - } - } fiber = fiber.return; } - if (!root) { - throw new Error('Could not find root from the boundary.'); - } - return root; } function performSynchronousWorkUnsafe() { @@ -543,16 +528,15 @@ module.exports = function(config : HostConfig) { try { if (priorityLevel === SynchronousPriority) { performSynchronousWorkUnsafe(); - } - if (priorityLevel > AnimationPriority) { + } else if (priorityLevel > AnimationPriority) { if (!deadline) { throw new Error('No deadline'); } else { performDeferredWorkUnsafe(deadline); } - return; + } else { + performAnimationWorkUnsafe(); } - performAnimationWorkUnsafe(); } catch (error) { const failedUnitOfWork = nextUnitOfWork; // Reset because it points to the error boundary: @@ -572,6 +556,7 @@ module.exports = function(config : HostConfig) { function handleErrors() : void { let firstUncaughtError = null; + let rootsWithUncaughtErrors = null; // In each phase, we will attempt to pass errors to boundaries and re-render them. // If we get more errors, we propagate them to higher boundaries in the next iterations. @@ -580,22 +565,25 @@ module.exports = function(config : HostConfig) { nextTrappedErrors = null; // Pass errors to all affected boundaries. - const affectedBoundaries : Set = new Set(); + const affectedRootsByBondary : Map = new Map(); trappedErrors.forEach(trappedError => { const boundary = trappedError.boundary; const error = trappedError.error; + const root = trappedError.root; if (!boundary) { firstUncaughtError = firstUncaughtError || error; + rootsWithUncaughtErrors = rootsWithUncaughtErrors || new Set(); + rootsWithUncaughtErrors.add(root); return; } // Don't visit boundaries twice. - if (affectedBoundaries.has(boundary)) { + if (affectedRootsByBondary.has(boundary)) { return; } // Give error boundary a chance to update its state. try { acknowledgeErrorInBoundary(boundary, error); - affectedBoundaries.add(boundary); + affectedRootsByBondary.set(boundary, root); } catch (nextError) { // If it throws, propagate the error. nextTrappedErrors = nextTrappedErrors || []; @@ -604,9 +592,9 @@ module.exports = function(config : HostConfig) { }); // We will process an update caused by each error boundary synchronously. - affectedBoundaries.forEach(boundary => { + affectedRootsByBondary.forEach((root, boundary) => { const priority = priorityContext; - const root = scheduleErrorBoundaryWork(boundary, priority); + scheduleErrorBoundaryWork(boundary, priority); // This should use findNextUnitOfWork() when synchronous scheduling is implemented. let fiber = cloneFiber(root.current, priority); try { @@ -623,15 +611,40 @@ module.exports = function(config : HostConfig) { ReactCurrentOwner.current = null; + // Unschedule any roots with uncaught errors + if (rootsWithUncaughtErrors) { + // Filter the linked list + let newNextScheduledRoot = null; + let newLastScheduledRoot = null; + // Find first root that has no errors + let root = nextScheduledRoot; + while (root) { + if (rootsWithUncaughtErrors.has(root)) { + root.isScheduled = false; + root = root.nextScheduledRoot; + } else { + newNextScheduledRoot = root; + newLastScheduledRoot = root; + break; + } + } + // Find the rest of the roots with no errors + while (root) { + if (root.nextScheduledRoot && rootsWithUncaughtErrors.has(root.nextScheduledRoot)) { + root.nextScheduledRoot.isScheduled = false; + root.nextScheduledRoot = root.nextScheduledRoot.nextScheduledRoot; + } else { + newLastScheduledRoot = root; + root = root.nextScheduledRoot; + } + } + // Update the first and last pointers + nextScheduledRoot = newNextScheduledRoot; + lastScheduledRoot = newLastScheduledRoot; + } + // Surface the first error uncaught by the boundaries to the user. if (firstUncaughtError) { - // We need to make sure any future root can get scheduled despite these errors. - // Currently after throwing, nothing gets scheduled because these fields are set. - // FIXME: this is likely a wrong fix! It's still better than ignoring updates though. - nextScheduledRoot = null; - lastScheduledRoot = null; - - // Throw any unhandled errors. throw firstUncaughtError; } } From 6dabd7401776df56aa75205b43f19254d9003d90 Mon Sep 17 00:00:00 2001 From: Dan Abramov Date: Mon, 31 Oct 2016 21:29:19 +0000 Subject: [PATCH 14/14] Test that fatal errors don't break scheduling --- .../ReactIncrementalErrorHandling.js | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js diff --git a/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js new file mode 100644 index 0000000000000..17b9a9ab36d7c --- /dev/null +++ b/src/renderers/shared/fiber/__tests__/ReactIncrementalErrorHandling.js @@ -0,0 +1,111 @@ +/** + * 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. + * + * @emails react-core + */ + +'use strict'; + +describe('ReactIncrementalErrorBoundaries', () => { + var React; + var ReactNoop; + var ErrorBoundary; + + beforeEach(() => { + React = require('React'); + ReactNoop = require('ReactNoop'); + }); + + it('can schedule updates after crashing in render on mount', () => { + var ops = []; + + function BrokenRender() { + ops.push('BrokenRender'); + throw new Error('Hello.'); + } + + function Foo() { + ops.push('Foo'); + return null; + } + + ReactNoop.render(); + expect(() => { + ReactNoop.flush(); + }).toThrow('Hello.'); + expect(ops).toEqual(['BrokenRender']); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + }); + + it('can schedule updates after crashing in render on update', () => { + var ops = []; + + function BrokenRender(props) { + ops.push('BrokenRender'); + if (props.throw) { + throw new Error('Hello.'); + } + } + + function Foo() { + ops.push('Foo'); + return null; + } + + ReactNoop.render(); + ReactNoop.flush(); + ops = []; + + expect(() => { + ReactNoop.render(); + ReactNoop.flush(); + }).toThrow('Hello.'); + expect(ops).toEqual(['BrokenRender']); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + }); + + it('can schedule updates after crashing during umounting', () => { + var ops = []; + + class BrokenComponentWillUnmount extends React.Component { + render() { + return
; + } + componentWillUnmount() { + throw new Error('Hello.'); + } + } + + function Foo() { + ops.push('Foo'); + return null; + } + + ReactNoop.render(); + ReactNoop.flush(); + + expect(() => { + ReactNoop.render(
); + ReactNoop.flush(); + }).toThrow('Hello.'); + + ops = []; + ReactNoop.render(); + ReactNoop.flush(); + expect(ops).toEqual(['Foo']); + }); + +});