From ecf468e5567586192d61b0e426398878cb5cefcb Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 28 Oct 2016 13:26:25 -0700 Subject: [PATCH] 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 8294883db5..ca3b1f703b 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 d775860d24..7652ce1464 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 139fe9d8bf..f93a849bc6 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; @@ -490,9 +493,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) { + nextUnitOfWork = findNextUnitOfWork(); + } } if (nextUnitOfWork) { if (nextPriorityLevel > AnimationPriority) { @@ -504,7 +508,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) { @@ -560,10 +592,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); @@ -604,8 +635,7 @@ module.exports = function(config : HostConfig) { } if (priorityLevel === SynchronousPriority) { - root.current.pendingWorkPriority = SynchronousPriority; - performSynchronousWork(); + scheduleSynchronousWork(root); } if (priorityLevel === NoWork) { @@ -626,15 +656,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 d352dd82c5..8e27e73981 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; }