mirror of
https://github.com/facebook/react.git
synced 2025-11-01 09:12:30 +00:00
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.
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<T, P, I, TI, C, CX>(
|
||||
} 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<T, P, I, TI, C, CX>(
|
||||
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<T, P, I, TI, C, CX>(
|
||||
} 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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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<T, P, I, TI, C, CX>(
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -41,7 +41,6 @@ var {
|
||||
} = ReactTypeOfWork;
|
||||
var {
|
||||
Update,
|
||||
Callback,
|
||||
} = ReactTypeOfSideEffect;
|
||||
|
||||
if (__DEV__) {
|
||||
@@ -73,11 +72,6 @@ module.exports = function<T, P, I, TI, C, CX>(
|
||||
workInProgress.effectTag |= Update;
|
||||
}
|
||||
|
||||
function markCallback(workInProgress : Fiber) {
|
||||
// Tag the fiber with a callback effect.
|
||||
workInProgress.effectTag |= Callback;
|
||||
}
|
||||
|
||||
function appendAllYields(yields : Array<ReifiedYield>, workInProgress : Fiber) {
|
||||
let node = workInProgress.child;
|
||||
while (node) {
|
||||
@@ -187,26 +181,11 @@ module.exports = function<T, P, I, TI, C, CX>(
|
||||
// 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<T, P, I, TI, C, CX>(
|
||||
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:
|
||||
|
||||
@@ -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<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
|
||||
mountContainer(element : ReactElement<any>, containerInfo : C, parentComponent : ?ReactComponent<any, any, any>, 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<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
|
||||
// It may seem strange that we don't return the root here, but that will
|
||||
// allow us to have containers that are in the middle of the tree instead
|
||||
// of being roots.
|
||||
return container;
|
||||
return current;
|
||||
},
|
||||
|
||||
updateContainer(element : ReactElement<any>, container : OpaqueNode, parentComponent : ?ReactComponent<any, any, any>, 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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -392,7 +392,6 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
|
||||
// The work is now done. We don't need this anymore. This flags
|
||||
// to the system not to redo any work here.
|
||||
workInProgress.pendingProps = null;
|
||||
workInProgress.updateQueue = null;
|
||||
|
||||
const returnFiber = workInProgress.return;
|
||||
const siblingFiber = workInProgress.sibling;
|
||||
|
||||
@@ -12,100 +12,252 @@
|
||||
|
||||
'use strict';
|
||||
|
||||
type UpdateQueueNode = {
|
||||
partialState: any,
|
||||
callback: ?Function,
|
||||
import type { Fiber } from 'ReactFiber';
|
||||
|
||||
type PartialState<State, Props> =
|
||||
$Subtype<State> |
|
||||
(prevState: State, props: Props) => $Subtype<State>;
|
||||
|
||||
type Callback = () => void;
|
||||
|
||||
type Update = {
|
||||
partialState: PartialState<any, any>,
|
||||
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<any, any> | 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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user