From cbba5ef9cc46564cd8afb981a98b33fe5b62e2e6 Mon Sep 17 00:00:00 2001 From: Andrew Clark Date: Fri, 9 Dec 2016 00:32:03 -0800 Subject: [PATCH] Don't drop updates until they are committed Restructures the update queue to maintain a pointer to the first pending update, which solves a few problems: - Updates that occur during the begin phase (e.g. in cWRP of a child) aren't dropped, like they are currently. This isn't working yet because the work priority is reset during completion. The following item will fix it. - Sets us up to be able to add separate priorities to each update in the queue. I'll add this in a subsequent commit. --- src/renderers/noop/ReactNoop.js | 14 +- src/renderers/shared/fiber/ReactFiber.js | 11 +- .../shared/fiber/ReactFiberBeginWork.js | 67 +++- .../shared/fiber/ReactFiberClassComponent.js | 49 ++- .../shared/fiber/ReactFiberCommitWork.js | 24 +- .../shared/fiber/ReactFiberCompleteWork.js | 28 +- .../shared/fiber/ReactFiberReconciler.js | 55 ++-- src/renderers/shared/fiber/ReactFiberRoot.js | 3 - .../shared/fiber/ReactFiberScheduler.js | 1 - .../shared/fiber/ReactFiberUpdateQueue.js | 306 +++++++++++++----- 10 files changed, 363 insertions(+), 195 deletions(-) diff --git a/src/renderers/noop/ReactNoop.js b/src/renderers/noop/ReactNoop.js index d16b6e7bf2..c3ca7bc633 100644 --- a/src/renderers/noop/ReactNoop.js +++ b/src/renderers/noop/ReactNoop.js @@ -284,17 +284,19 @@ var ReactNoop = { function logUpdateQueue(updateQueue : UpdateQueue, depth) { log( - ' '.repeat(depth + 1) + 'QUEUED UPDATES', - updateQueue.isReplace ? 'is replace' : '', - updateQueue.isForced ? 'is forced' : '' + ' '.repeat(depth + 1) + 'QUEUED UPDATES' ); + const firstPendingUpdate = updateQueue.firstPendingUpdate; + if (!firstPendingUpdate) { + return; + } log( ' '.repeat(depth + 1) + '~', - updateQueue.partialState, - updateQueue.callback ? 'with callback' : '' + firstPendingUpdate && firstPendingUpdate.partialState, + firstPendingUpdate.callback ? 'with callback' : '' ); var next; - while (next = updateQueue.next) { + while (next = firstPendingUpdate.next) { log( ' '.repeat(depth + 1) + '~', next.partialState, diff --git a/src/renderers/shared/fiber/ReactFiber.js b/src/renderers/shared/fiber/ReactFiber.js index 8873e2b46c..5145e1a995 100644 --- a/src/renderers/shared/fiber/ReactFiber.js +++ b/src/renderers/shared/fiber/ReactFiber.js @@ -100,12 +100,11 @@ export type Fiber = { pendingProps: any, // This type will be more specific once we overload the tag. // TODO: I think that there is a way to merge pendingProps and memoizedProps. memoizedProps: any, // The props used to create the output. - // A queue of local state updates. - updateQueue: ?UpdateQueue, - // The state used to create the output. This is a full state object. + + // A queue of state updates and callbacks. + updateQueue: UpdateQueue | null, + // The state used to create the output memoizedState: any, - // Linked list of callbacks to call after updates are committed. - callbackList: ?UpdateQueue, // Effect effectTag: TypeOfSideEffect, @@ -195,7 +194,6 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber { memoizedProps: null, updateQueue: null, memoizedState: null, - callbackList: null, effectTag: NoEffect, nextEffect: null, @@ -271,7 +269,6 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi // TODO: Pass in the new pendingProps as an argument maybe? alt.pendingProps = fiber.pendingProps; alt.updateQueue = fiber.updateQueue; - alt.callbackList = fiber.callbackList; alt.pendingWorkPriority = priorityLevel; alt.memoizedProps = fiber.memoizedProps; diff --git a/src/renderers/shared/fiber/ReactFiberBeginWork.js b/src/renderers/shared/fiber/ReactFiberBeginWork.js index bc98949e6f..fcc89e0532 100644 --- a/src/renderers/shared/fiber/ReactFiberBeginWork.js +++ b/src/renderers/shared/fiber/ReactFiberBeginWork.js @@ -25,7 +25,10 @@ var { reconcileChildFibersInPlace, cloneChildFibers, } = require('ReactChildFiber'); - +var { + hasPendingUpdate, + mergeQueue, +} = require('ReactFiberUpdateQueue'); var ReactTypeOfWork = require('ReactTypeOfWork'); var { getMaskedContext, @@ -53,6 +56,8 @@ var { OffscreenPriority, } = require('ReactPriorityLevel'); var { + Update, + Callback, Placement, ContentReset, Err, @@ -210,9 +215,29 @@ module.exports = function( } else { shouldUpdate = updateClassInstance(current, workInProgress); } - if (!shouldUpdate) { + + // Schedule side-effects + const updateQueue = workInProgress.updateQueue; + if (updateQueue && updateQueue.hasCallback) { + // The update queue has a callback. Schedule a callback effect. + // Callbacks are scheduled regardless of whether we bail out below. + workInProgress.effectTag |= Callback; + } + if (shouldUpdate) { + workInProgress.effectTag |= Update; + } else { + // If an update was already in progress, we should schedule an Update + // effect even though we're bailing out, so that cWU/cDU are called. + if (current) { + const instance = current.stateNode; + if (instance.props !== current.memoizedProps || + instance.state !== current.memoizedState) { + workInProgress.effectTag |= Update; + } + } return bailoutOnAlreadyFinishedWork(current, workInProgress); } + // Rerender const instance = workInProgress.stateNode; ReactCurrentOwner.current = workInProgress; @@ -471,13 +496,24 @@ module.exports = function( workInProgress.child = workInProgress.progressedChild; } - if ((workInProgress.pendingProps === null || ( - workInProgress.memoizedProps !== null && - workInProgress.pendingProps === workInProgress.memoizedProps - )) && - workInProgress.updateQueue === null && - !hasContextChanged()) { - return bailoutOnAlreadyFinishedWork(current, workInProgress); + const pendingProps = workInProgress.pendingProps; + const memoizedProps = workInProgress.memoizedProps; + const updateQueue = workInProgress.updateQueue; + + + + // This is kept as a single expression to take advantage of short-circuiting. + const hasNewProps = ( + pendingProps !== null && ( // hasPendingProps && ( + memoizedProps === null || // hasNoMemoizedProps || + pendingProps !== memoizedProps // memoizedPropsDontMatch + ) // ) + ); + if (!hasNewProps) { + const hasUpdate = updateQueue && hasPendingUpdate(updateQueue); + if (!hasUpdate && !hasContextChanged()) { + return bailoutOnAlreadyFinishedWork(current, workInProgress); + } } switch (workInProgress.tag) { @@ -497,8 +533,19 @@ module.exports = function( } else { pushTopLevelContextObject(root.context, false); } + + if (updateQueue) { + // The last three arguments are unimportant because there should be + // no update functions in a HostRoot's queue. + mergeQueue(updateQueue, null, null, null); + if (updateQueue.hasCallback) { + workInProgress.effectTag |= Callback; + } + } + pushHostContainer(workInProgress.stateNode.containerInfo); - reconcileChildren(current, workInProgress, workInProgress.pendingProps); + reconcileChildren(current, workInProgress, pendingProps); + // A yield component is just a placeholder, we can just run through the // next one immediately. return workInProgress.child; diff --git a/src/renderers/shared/fiber/ReactFiberClassComponent.js b/src/renderers/shared/fiber/ReactFiberClassComponent.js index 79595dd152..b72c6bb777 100644 --- a/src/renderers/shared/fiber/ReactFiberClassComponent.js +++ b/src/renderers/shared/fiber/ReactFiberClassComponent.js @@ -20,9 +20,11 @@ var { } = require('ReactFiberContext'); var { createUpdateQueue, - addToQueue, - addCallbackToQueue, - mergeUpdateQueue, + addUpdate, + addReplaceUpdate, + addForceUpdate, + addCallback, + mergeQueue, } = require('ReactFiberUpdateQueue'); var { getComponentName, isMounted } = require('ReactFiberTreeReflection'); var ReactInstanceMap = require('ReactInstanceMap'); @@ -49,36 +51,33 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { isMounted, enqueueSetState(instance, partialState) { const fiber = ReactInstanceMap.get(instance); - const updateQueue = fiber.updateQueue ? - addToQueue(fiber.updateQueue, partialState) : - createUpdateQueue(partialState); - scheduleUpdateQueue(fiber, updateQueue); + const queue = fiber.updateQueue || createUpdateQueue(); + addUpdate(queue, partialState); + scheduleUpdateQueue(fiber, queue); }, enqueueReplaceState(instance, state) { const fiber = ReactInstanceMap.get(instance); - const updateQueue = createUpdateQueue(state); - updateQueue.isReplace = true; - scheduleUpdateQueue(fiber, updateQueue); + const queue = fiber.updateQueue || createUpdateQueue(); + addReplaceUpdate(queue, state); + scheduleUpdateQueue(fiber, queue); }, enqueueForceUpdate(instance) { const fiber = ReactInstanceMap.get(instance); - const updateQueue = fiber.updateQueue || createUpdateQueue(null); - updateQueue.isForced = true; - scheduleUpdateQueue(fiber, updateQueue); + const queue = fiber.updateQueue || createUpdateQueue(); + addForceUpdate(queue); + scheduleUpdateQueue(fiber, queue); }, enqueueCallback(instance, callback) { const fiber = ReactInstanceMap.get(instance); - let updateQueue = fiber.updateQueue ? - fiber.updateQueue : - createUpdateQueue(null); - addCallbackToQueue(updateQueue, callback); - scheduleUpdateQueue(fiber, updateQueue); + const queue = fiber.updateQueue || createUpdateQueue(); + addCallback(queue, callback); + scheduleUpdateQueue(fiber, queue); }, }; function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) { const updateQueue = workInProgress.updateQueue; - if (oldProps === null || (updateQueue && updateQueue.isForced)) { + if (oldProps === null || (updateQueue && updateQueue.hasForceUpdate)) { return true; } @@ -245,7 +244,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { // process them now. const updateQueue = workInProgress.updateQueue; if (updateQueue) { - instance.state = mergeUpdateQueue(updateQueue, instance, state, props); + instance.state = mergeQueue(updateQueue, instance, state, props); } } } @@ -294,7 +293,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { // during initial mounting. const newUpdateQueue = workInProgress.updateQueue; if (newUpdateQueue) { - newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps); + newInstance.state = mergeQueue(newUpdateQueue, newInstance, newState, newProps); } return true; } @@ -332,11 +331,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { // TODO: Previous state can be null. let newState; if (updateQueue) { - if (!updateQueue.hasUpdate) { - newState = oldState; - } else { - newState = mergeUpdateQueue(updateQueue, instance, oldState, newProps); - } + newState = mergeQueue(updateQueue, instance, oldState, newProps); } else { newState = oldState; } @@ -344,7 +339,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) { if (oldProps === newProps && oldState === newState && oldContext === newContext && - updateQueue && !updateQueue.isForced) { + updateQueue && !updateQueue.hasForceUpdate) { return false; } diff --git a/src/renderers/shared/fiber/ReactFiberCommitWork.js b/src/renderers/shared/fiber/ReactFiberCommitWork.js index 5ff31e2d51..9c1bf2a9f1 100644 --- a/src/renderers/shared/fiber/ReactFiberCommitWork.js +++ b/src/renderers/shared/fiber/ReactFiberCommitWork.js @@ -25,12 +25,11 @@ var { HostPortal, CoroutineComponent, } = ReactTypeOfWork; -var { callCallbacks } = require('ReactFiberUpdateQueue'); +var { commitUpdateQueue } = require('ReactFiberUpdateQueue'); var { Placement, Update, - Callback, ContentReset, } = require('ReactTypeOfSideEffect'); @@ -418,25 +417,16 @@ module.exports = function( } attachRef(current, finishedWork, instance); } - // Clear updates from current fiber. - if (finishedWork.alternate) { - finishedWork.alternate.updateQueue = null; - } - if (finishedWork.effectTag & Callback) { - if (finishedWork.callbackList) { - const callbackList = finishedWork.callbackList; - finishedWork.callbackList = null; - callCallbacks(callbackList, instance); - } + if (finishedWork.updateQueue) { + commitUpdateQueue(finishedWork, finishedWork.updateQueue, instance); } return; } case HostRoot: { - const rootFiber = finishedWork.stateNode; - if (rootFiber.callbackList) { - const callbackList = rootFiber.callbackList; - rootFiber.callbackList = null; - callCallbacks(callbackList, rootFiber.current.child.stateNode); + const updateQueue = finishedWork.updateQueue; + if (updateQueue) { + const instance = finishedWork.child && finishedWork.child.stateNode; + commitUpdateQueue(finishedWork, updateQueue, instance); } return; } diff --git a/src/renderers/shared/fiber/ReactFiberCompleteWork.js b/src/renderers/shared/fiber/ReactFiberCompleteWork.js index d0a6ab217d..93e61ce0c1 100644 --- a/src/renderers/shared/fiber/ReactFiberCompleteWork.js +++ b/src/renderers/shared/fiber/ReactFiberCompleteWork.js @@ -41,7 +41,6 @@ var { } = ReactTypeOfWork; var { Update, - Callback, } = ReactTypeOfSideEffect; if (__DEV__) { @@ -73,11 +72,6 @@ module.exports = function( workInProgress.effectTag |= Update; } - function markCallback(workInProgress : Fiber) { - // Tag the fiber with a callback effect. - workInProgress.effectTag |= Callback; - } - function appendAllYields(yields : Array, workInProgress : Fiber) { let node = workInProgress.child; while (node) { @@ -187,26 +181,11 @@ module.exports = function( // Don't use the state queue to compute the memoized state. We already // merged it and assigned it to the instance. Transfer it from there. // Also need to transfer the props, because pendingProps will be null - // in the case of an update + // in the case of an update. const { state, props } = workInProgress.stateNode; - const updateQueue = workInProgress.updateQueue; workInProgress.memoizedState = state; workInProgress.memoizedProps = props; - if (current) { - if (current.memoizedProps !== workInProgress.memoizedProps || - 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 HostRoot: { workInProgress.memoizedProps = workInProgress.pendingProps; @@ -215,9 +194,6 @@ module.exports = function( fiberRoot.context = fiberRoot.pendingContext; fiberRoot.pendingContext = null; } - // TODO: Only mark this as an update if we have any pending callbacks - // on it. - markUpdate(workInProgress); return null; } case HostComponent: diff --git a/src/renderers/shared/fiber/ReactFiberReconciler.js b/src/renderers/shared/fiber/ReactFiberReconciler.js index 582d7946e3..18c98771ef 100644 --- a/src/renderers/shared/fiber/ReactFiberReconciler.js +++ b/src/renderers/shared/fiber/ReactFiberReconciler.js @@ -24,7 +24,7 @@ var { var { createFiberRoot } = require('ReactFiberRoot'); var ReactFiberScheduler = require('ReactFiberScheduler'); -var { createUpdateQueue, addCallbackToQueue } = require('ReactFiberUpdateQueue'); +var { createUpdateQueue, addCallback } = require('ReactFiberUpdateQueue'); if (__DEV__) { var ReactFiberInstrumentation = require('ReactFiberInstrumentation'); @@ -109,16 +109,23 @@ module.exports = function(config : HostConfig, containerInfo : C, parentComponent : ?ReactComponent, callback: ?Function) : OpaqueNode { const context = getContextForSubtree(parentComponent); const root = createFiberRoot(containerInfo, context); - const container = root.current; - if (callback) { - const queue = createUpdateQueue(null); - addCallbackToQueue(queue, callback); - root.callbackList = queue; - } - // TODO: Use pending work/state instead of props. + const current = root.current; + + // TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps. // TODO: This should not override the pendingWorkPriority if there is // higher priority work in the subtree. - container.pendingProps = element; + current.pendingProps = element; + if (current.alternate) { + current.alternate.pendingProps = element; + } + if (callback) { + const queue = current.updateQueue || createUpdateQueue(); + addCallback(queue, callback); + current.updateQueue = queue; + if (current.alternate) { + current.alternate.updateQueue = queue; + } + } scheduleWork(root); @@ -129,24 +136,30 @@ module.exports = function(config : HostConfig, container : OpaqueNode, parentComponent : ?ReactComponent, callback: ?Function) : void { // TODO: If this is a nested container, this won't be the root. const root : FiberRoot = (container.stateNode : any); - if (callback) { - const queue = root.callbackList ? - root.callbackList : - createUpdateQueue(null); - addCallbackToQueue(queue, callback); - root.callbackList = queue; - } + const current = root.current; + root.pendingContext = getContextForSubtree(parentComponent); - // TODO: Use pending work/state instead of props. - root.current.pendingProps = element; - if (root.current.alternate) { - root.current.alternate.pendingProps = element; + + // TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps. + // TODO: This should not override the pendingWorkPriority if there is + // higher priority work in the subtree. + current.pendingProps = element; + if (current.alternate) { + current.alternate.pendingProps = element; + } + if (callback) { + const queue = current.updateQueue || createUpdateQueue(); + addCallback(queue, callback); + current.updateQueue = queue; + if (current.alternate) { + current.alternate.updateQueue = queue; + } } scheduleWork(root); diff --git a/src/renderers/shared/fiber/ReactFiberRoot.js b/src/renderers/shared/fiber/ReactFiberRoot.js index 4da3b9d18e..47ef945dab 100644 --- a/src/renderers/shared/fiber/ReactFiberRoot.js +++ b/src/renderers/shared/fiber/ReactFiberRoot.js @@ -13,7 +13,6 @@ 'use strict'; import type { Fiber } from 'ReactFiber'; -import type { UpdateQueue } from 'ReactFiberUpdateQueue'; const { createHostRootFiber } = require('ReactFiber'); @@ -26,8 +25,6 @@ export type FiberRoot = { isScheduled: boolean, // The work schedule is a linked list. nextScheduledRoot: ?FiberRoot, - // Linked list of callbacks to call after updates are committed. - callbackList: ?UpdateQueue, // Top context object, used by renderSubtreeIntoContainer context: Object, pendingContext: ?Object, diff --git a/src/renderers/shared/fiber/ReactFiberScheduler.js b/src/renderers/shared/fiber/ReactFiberScheduler.js index 7bf2f7d7aa..1e858df085 100644 --- a/src/renderers/shared/fiber/ReactFiberScheduler.js +++ b/src/renderers/shared/fiber/ReactFiberScheduler.js @@ -392,7 +392,6 @@ module.exports = function(config : HostConfig = + $Subtype | + (prevState: State, props: Props) => $Subtype; + +type Callback = () => void; + +type Update = { + partialState: PartialState, + callback: Callback | null, isReplace: boolean, - next: ?UpdateQueueNode, -}; - -export type UpdateQueue = UpdateQueueNode & { isForced: boolean, - hasUpdate: boolean, + next: Update | null, +}; + +export type UpdateQueue = { + // Points to the first (oldest) update. + first: Update | null, + // Points to the first pending update. A pending update is one that is not + // part of the progressed work. This could be null even in a non-empty queue, + // when none of the updates are empty. + firstPendingUpdate: Update | null, + // Points to the last (newest) update. + last: Update | null, + + // Used to implement forceUpdate. Only true if there's a merged (non-pending) + // force update; pending force updates do not affect this. + hasForceUpdate: boolean, + + // TODO: Remove this by scheduling the side-effect during the begin phase. hasCallback: boolean, - tail: UpdateQueueNode }; -exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue { - const queue = { - partialState, - callback: null, - isReplace: false, - next: null, - isForced: false, - hasUpdate: partialState != null, +exports.createUpdateQueue = function() : UpdateQueue { + return { + first: null, + firstPendingUpdate: null, + last: null, + + hasForceUpdate: false, hasCallback: false, - tail: (null : any), }; - queue.tail = queue; - return queue; }; -function addToQueue(queue : UpdateQueue, partialState : mixed) : UpdateQueue { - const node = { - partialState, - callback: null, - isReplace: false, - next: null, - }; - queue.tail.next = node; - queue.tail = node; - queue.hasUpdate = queue.hasUpdate || (partialState != null); - return queue; +function insertUpdateIntoQueue(queue : UpdateQueue, update : Update) : void { + // Add a pending update to the end of the queue. + // TODO: Once updates have priorities, they should be inserted in the + // correct order. Addtionally, replaceState should remove any pending updates + // that have lower priority from queue. + if (!queue.last) { + // The queue is empty. + queue.first = queue.last = queue.firstPendingUpdate = update; + } else { + // The queue is not empty. Append the update to the end. + queue.last.next = update; + queue.last = update; + + if (!queue.firstPendingUpdate) { + // This is the first pending update. Update the pointer. + queue.firstPendingUpdate = update; + } + } } -exports.addToQueue = addToQueue; - -exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : UpdateQueue { - if (queue.tail.callback) { - // If the tail already as a callback, add an empty node to queue - addToQueue(queue, null); - } - queue.tail.callback = callback; - queue.hasCallback = true; - return queue; +exports.addUpdate = function(queue : UpdateQueue, partialState : PartialState | null) : void { + const update = { + partialState, + callback: null, + isReplace: false, + isForced: false, + next: null, + }; + insertUpdateIntoQueue(queue, update); }; -exports.callCallbacks = function(queue : UpdateQueue, context : any) { - let node : ?UpdateQueueNode = queue; - while (node) { - const callback = node.callback; - if (callback) { - if (typeof context !== 'undefined') { - callback.call(context); - } else { - callback(); +exports.addReplaceUpdate = function(queue : UpdateQueue, state : any | null) : void { + const replaceUpdate = { + partialState: state, + callback: null, + isReplace: true, + isForced: false, + next: null, + }; + + + if (!queue.last) { + // The queue is empty. + queue.first = queue.last = queue.firstPendingUpdate = replaceUpdate; + } else { + // The queue is not empty. + + // Drop all existing pending updates. + // TODO: Only drop updates with matching priority. + let lastMergedUpdate = null; + if (queue.firstPendingUpdate) { + let node = queue.first; + while (node && node.next !== queue.firstPendingUpdate) { + node = node.next; + } + lastMergedUpdate = node; + } else { + lastMergedUpdate = queue.last; + } + + if (lastMergedUpdate) { + // Append the new update to the end of the list. + // $FlowFixMe: Union bug (I think? Getting "object literal - This type incompatible with null") + lastMergedUpdate.next = replaceUpdate; + queue.firstPendingUpdate = replaceUpdate; + queue.last = replaceUpdate; + } else { + // Drop everything + queue.first = queue.firstPendingUpdate = queue.last = replaceUpdate; + } + } + +}; + +exports.addForceUpdate = function(queue : UpdateQueue) : void { + const update = { + partialState: null, + callback: null, + isReplace: false, + isForced: true, + next: null, + }; + insertUpdateIntoQueue(queue, update); +}; + + +exports.addCallback = function(queue : UpdateQueue, callback: Callback) : void { + if (queue.firstPendingUpdate && queue.last && !queue.last.callback) { + // If pending updates already exist, and the last pending update does not + // have a callback, we can add the new callback to that update. + // TODO: Add an additional check to ensure the priority matches. + queue.last.callback = callback; + return; + } + + const update = { + partialState: null, + callback, + isReplace: false, + isForced: false, + next: null, + }; + insertUpdateIntoQueue(queue, update); +}; + +exports.hasPendingUpdate = function(queue : UpdateQueue) : boolean { + // TODO: Check priority level + return queue.firstPendingUpdate !== null; +}; + +function getStateFromUpdate(update, instance, prevState, props) { + const partialState = update.partialState; + if (typeof partialState === 'function') { + const updateFn = partialState; + return updateFn.call(instance, prevState, props); + } else { + return partialState; + } +} + +// TODO: Move callback effect scheduling here. Rename to beginUpdateQueue or similar. +exports.mergeQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any { + // This merges the entire update queue into a single object, not just the + // pending updates, because the previous state and props may have changed. + // TODO: Would memoization be worth it? + + // Reset these flags. We'll update them while looping through the queue. + queue.hasForceUpdate = false; + queue.hasCallback = false; + + let state = prevState; + let dontMutatePrevState = true; + let update : Update | null = queue.first; + let isEmpty = true; + + // TODO: Stop merging once we reach an update whose priority doesn't match. + // Should this also apply to updates that were previous merged but bailed out? + while (update) { + let partialState; + if (update.isReplace) { + // A replace should drop all previous updates in the queue, so + // use the original `prevState`, not the accumulated `state` + state = getStateFromUpdate(update, instance, prevState, props); + dontMutatePrevState = true; + isEmpty = false; + } else { + partialState = getStateFromUpdate(update, instance, state, props); + if (partialState) { + if (dontMutatePrevState) { + state = Object.assign({}, state, partialState); + } else { + state = Object.assign(state, partialState); + } + dontMutatePrevState = false; + isEmpty = false; } } - node = node.next; - } -}; - -function getStateFromNode(node, instance, state, props) { - if (typeof node.partialState === 'function') { - const updateFn = node.partialState; - return updateFn.call(instance, state, props); - } else { - return node.partialState; - } -} - -exports.mergeUpdateQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any { - let node : ?UpdateQueueNode = queue; - if (queue.isReplace) { - // replaceState is always first in the queue. - prevState = getStateFromNode(queue, instance, prevState, props); - node = queue.next; - if (!node) { - // If there is no more work, we replace the raw object instead of cloning. - return prevState; + if (update.isForced) { + queue.hasForceUpdate = true; } + if (update.callback) { + queue.hasCallback = true; + } + update = update.next; } - let state = Object.assign({}, prevState); - while (node) { - let partialState = getStateFromNode(node, instance, state, props); - Object.assign(state, partialState); - node = node.next; + + // The next pending update is the one that we exited on in the loop above. + // Until priorities are implemented, this is always null. + queue.firstPendingUpdate = update; + + if (isEmpty) { + // None of the updates contained state. Return the original state object. + return prevState; } + return state; }; + +exports.commitUpdateQueue = function(finishedWork : Fiber, queue : UpdateQueue, context : mixed) { + if (queue.hasCallback) { + // Call the callbacks on all the non-pending updates. + let update = queue.first; + while (update && update !== queue.firstPendingUpdate) { + const callback = update.callback; + if (typeof callback === 'function') { + callback.call(context); + } + update = update.next; + } + } + + // Drop all completed updates, leaving only the pending updates. + queue.first = queue.firstPendingUpdate; + if (!queue.first) { + // If the list is now empty, we can remove it from the finished work + finishedWork.updateQueue = null; + if (finishedWork.alternate) { + // Normally we don't mutate the current tree, but we do for updates. + // The queue on the work in progress is always the same as the queue + // on the current. + finishedWork.alternate.updateQueue = null; + } + } +};