@samitouri / QOS-React-2 / commits / cbba5ef9cc

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.

Andrew Clark committed Dec 9, 2016 at 00:32 UTC cbba5ef9cc46564cd8afb981a98b33fe5b62e2e6
10 files changed +349 -181
src/renderers/noop/ReactNoop.js
+8 -6
@@ -284,17 +284,19 @@ var ReactNoop = {
284
285 function logUpdateQueue(updateQueue : UpdateQueue, depth) {
286 log(
287 - ' '.repeat(depth + 1) + 'QUEUED UPDATES',
288 - updateQueue.isReplace ? 'is replace' : '',
289 - updateQueue.isForced ? 'is forced' : ''
287 + ' '.repeat(depth + 1) + 'QUEUED UPDATES'
288 );
289 + const firstPendingUpdate = updateQueue.firstPendingUpdate;
290 + if (!firstPendingUpdate) {
291 + return;
292 + }
293 log(
294 ' '.repeat(depth + 1) + '~',
293 - updateQueue.partialState,
294 - updateQueue.callback ? 'with callback' : ''
295 + firstPendingUpdate && firstPendingUpdate.partialState,
296 + firstPendingUpdate.callback ? 'with callback' : ''
297 );
298 var next;
297 - while (next = updateQueue.next) {
299 + while (next = firstPendingUpdate.next) {
300 log(
301 ' '.repeat(depth + 1) + '~',
302 next.partialState,
src/renderers/shared/fiber/ReactFiber.js
+4 -7
@@ -100,12 +100,11 @@ export type Fiber = {
100 pendingProps: any, // This type will be more specific once we overload the tag.
101 // TODO: I think that there is a way to merge pendingProps and memoizedProps.
102 memoizedProps: any, // The props used to create the output.
103 - // A queue of local state updates.
104 - updateQueue: ?UpdateQueue,
105 - // The state used to create the output. This is a full state object.
103 +
104 + // A queue of state updates and callbacks.
105 + updateQueue: UpdateQueue | null,
106 + // The state used to create the output
107 memoizedState: any,
107 - // Linked list of callbacks to call after updates are committed.
108 - callbackList: ?UpdateQueue,
108
109 // Effect
110 effectTag: TypeOfSideEffect,
@@ -195,7 +194,6 @@ var createFiber = function(tag : TypeOfWork, key : null | string) : Fiber {
194 memoizedProps: null,
195 updateQueue: null,
196 memoizedState: null,
198 - callbackList: null,
197
198 effectTag: NoEffect,
199 nextEffect: null,
@@ -271,7 +269,6 @@ exports.cloneFiber = function(fiber : Fiber, priorityLevel : PriorityLevel) : Fi
269 // TODO: Pass in the new pendingProps as an argument maybe?
270 alt.pendingProps = fiber.pendingProps;
271 alt.updateQueue = fiber.updateQueue;
274 - alt.callbackList = fiber.callbackList;
272 alt.pendingWorkPriority = priorityLevel;
273
274 alt.memoizedProps = fiber.memoizedProps;
src/renderers/shared/fiber/ReactFiberBeginWork.js
+57 -10
@@ -25,7 +25,10 @@ var {
25 reconcileChildFibersInPlace,
26 cloneChildFibers,
27 } = require('ReactChildFiber');
28 -
28 +var {
29 + hasPendingUpdate,
30 + mergeQueue,
31 +} = require('ReactFiberUpdateQueue');
32 var ReactTypeOfWork = require('ReactTypeOfWork');
33 var {
34 getMaskedContext,
@@ -53,6 +56,8 @@ var {
56 OffscreenPriority,
57 } = require('ReactPriorityLevel');
58 var {
59 + Update,
60 + Callback,
61 Placement,
62 ContentReset,
63 Err,
@@ -210,9 +215,29 @@ module.exports = function<T, P, I, TI, C, CX>(
215 } else {
216 shouldUpdate = updateClassInstance(current, workInProgress);
217 }
213 - if (!shouldUpdate) {
218 +
219 + // Schedule side-effects
220 + const updateQueue = workInProgress.updateQueue;
221 + if (updateQueue && updateQueue.hasCallback) {
222 + // The update queue has a callback. Schedule a callback effect.
223 + // Callbacks are scheduled regardless of whether we bail out below.
224 + workInProgress.effectTag |= Callback;
225 + }
226 + if (shouldUpdate) {
227 + workInProgress.effectTag |= Update;
228 + } else {
229 + // If an update was already in progress, we should schedule an Update
230 + // effect even though we're bailing out, so that cWU/cDU are called.
231 + if (current) {
232 + const instance = current.stateNode;
233 + if (instance.props !== current.memoizedProps ||
234 + instance.state !== current.memoizedState) {
235 + workInProgress.effectTag |= Update;
236 + }
237 + }
238 return bailoutOnAlreadyFinishedWork(current, workInProgress);
239 }
240 +
241 // Rerender
242 const instance = workInProgress.stateNode;
243 ReactCurrentOwner.current = workInProgress;
@@ -471,13 +496,24 @@ module.exports = function<T, P, I, TI, C, CX>(
496 workInProgress.child = workInProgress.progressedChild;
497 }
498
474 - if ((workInProgress.pendingProps === null || (
475 - workInProgress.memoizedProps !== null &&
476 - workInProgress.pendingProps === workInProgress.memoizedProps
477 - )) &&
478 - workInProgress.updateQueue === null &&
479 - !hasContextChanged()) {
480 - return bailoutOnAlreadyFinishedWork(current, workInProgress);
499 + const pendingProps = workInProgress.pendingProps;
500 + const memoizedProps = workInProgress.memoizedProps;
501 + const updateQueue = workInProgress.updateQueue;
502 +
503 +
504 +
505 + // This is kept as a single expression to take advantage of short-circuiting.
506 + const hasNewProps = (
507 + pendingProps !== null && ( // hasPendingProps && (
508 + memoizedProps === null || // hasNoMemoizedProps ||
509 + pendingProps !== memoizedProps // memoizedPropsDontMatch
510 + ) // )
511 + );
512 + if (!hasNewProps) {
513 + const hasUpdate = updateQueue && hasPendingUpdate(updateQueue);
514 + if (!hasUpdate && !hasContextChanged()) {
515 + return bailoutOnAlreadyFinishedWork(current, workInProgress);
516 + }
517 }
518
519 switch (workInProgress.tag) {
@@ -497,8 +533,19 @@ module.exports = function<T, P, I, TI, C, CX>(
533 } else {
534 pushTopLevelContextObject(root.context, false);
535 }
536 +
537 + if (updateQueue) {
538 + // The last three arguments are unimportant because there should be
539 + // no update functions in a HostRoot's queue.
540 + mergeQueue(updateQueue, null, null, null);
541 + if (updateQueue.hasCallback) {
542 + workInProgress.effectTag |= Callback;
543 + }
544 + }
545 +
546 pushHostContainer(workInProgress.stateNode.containerInfo);
501 - reconcileChildren(current, workInProgress, workInProgress.pendingProps);
547 + reconcileChildren(current, workInProgress, pendingProps);
548 +
549 // A yield component is just a placeholder, we can just run through the
550 // next one immediately.
551 return workInProgress.child;
src/renderers/shared/fiber/ReactFiberClassComponent.js
+22 -27
@@ -20,9 +20,11 @@ var {
20 } = require('ReactFiberContext');
21 var {
22 createUpdateQueue,
23 - addToQueue,
24 - addCallbackToQueue,
25 - mergeUpdateQueue,
23 + addUpdate,
24 + addReplaceUpdate,
25 + addForceUpdate,
26 + addCallback,
27 + mergeQueue,
28 } = require('ReactFiberUpdateQueue');
29 var { getComponentName, isMounted } = require('ReactFiberTreeReflection');
30 var ReactInstanceMap = require('ReactInstanceMap');
@@ -49,36 +51,33 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
51 isMounted,
52 enqueueSetState(instance, partialState) {
53 const fiber = ReactInstanceMap.get(instance);
52 - const updateQueue = fiber.updateQueue ?
53 - addToQueue(fiber.updateQueue, partialState) :
54 - createUpdateQueue(partialState);
55 - scheduleUpdateQueue(fiber, updateQueue);
54 + const queue = fiber.updateQueue || createUpdateQueue();
55 + addUpdate(queue, partialState);
56 + scheduleUpdateQueue(fiber, queue);
57 },
58 enqueueReplaceState(instance, state) {
59 const fiber = ReactInstanceMap.get(instance);
59 - const updateQueue = createUpdateQueue(state);
60 - updateQueue.isReplace = true;
61 - scheduleUpdateQueue(fiber, updateQueue);
60 + const queue = fiber.updateQueue || createUpdateQueue();
61 + addReplaceUpdate(queue, state);
62 + scheduleUpdateQueue(fiber, queue);
63 },
64 enqueueForceUpdate(instance) {
65 const fiber = ReactInstanceMap.get(instance);
65 - const updateQueue = fiber.updateQueue || createUpdateQueue(null);
66 - updateQueue.isForced = true;
67 - scheduleUpdateQueue(fiber, updateQueue);
66 + const queue = fiber.updateQueue || createUpdateQueue();
67 + addForceUpdate(queue);
68 + scheduleUpdateQueue(fiber, queue);
69 },
70 enqueueCallback(instance, callback) {
71 const fiber = ReactInstanceMap.get(instance);
71 - let updateQueue = fiber.updateQueue ?
72 - fiber.updateQueue :
73 - createUpdateQueue(null);
74 - addCallbackToQueue(updateQueue, callback);
75 - scheduleUpdateQueue(fiber, updateQueue);
72 + const queue = fiber.updateQueue || createUpdateQueue();
73 + addCallback(queue, callback);
74 + scheduleUpdateQueue(fiber, queue);
75 },
76 };
77
78 function checkShouldComponentUpdate(workInProgress, oldProps, newProps, newState, newContext) {
79 const updateQueue = workInProgress.updateQueue;
81 - if (oldProps === null || (updateQueue && updateQueue.isForced)) {
80 + if (oldProps === null || (updateQueue && updateQueue.hasForceUpdate)) {
81 return true;
82 }
83
@@ -245,7 +244,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
244 // process them now.
245 const updateQueue = workInProgress.updateQueue;
246 if (updateQueue) {
248 - instance.state = mergeUpdateQueue(updateQueue, instance, state, props);
247 + instance.state = mergeQueue(updateQueue, instance, state, props);
248 }
249 }
250 }
@@ -294,7 +293,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
293 // during initial mounting.
294 const newUpdateQueue = workInProgress.updateQueue;
295 if (newUpdateQueue) {
297 - newInstance.state = mergeUpdateQueue(newUpdateQueue, newInstance, newState, newProps);
296 + newInstance.state = mergeQueue(newUpdateQueue, newInstance, newState, newProps);
297 }
298 return true;
299 }
@@ -332,11 +331,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
331 // TODO: Previous state can be null.
332 let newState;
333 if (updateQueue) {
335 - if (!updateQueue.hasUpdate) {
336 - newState = oldState;
337 - } else {
338 - newState = mergeUpdateQueue(updateQueue, instance, oldState, newProps);
339 - }
334 + newState = mergeQueue(updateQueue, instance, oldState, newProps);
335 } else {
336 newState = oldState;
337 }
@@ -344,7 +339,7 @@ module.exports = function(scheduleUpdate : (fiber: Fiber) => void) {
339 if (oldProps === newProps &&
340 oldState === newState &&
341 oldContext === newContext &&
347 - updateQueue && !updateQueue.isForced) {
342 + updateQueue && !updateQueue.hasForceUpdate) {
343 return false;
344 }
345
src/renderers/shared/fiber/ReactFiberCommitWork.js
+7 -17
@@ -25,12 +25,11 @@ var {
25 HostPortal,
26 CoroutineComponent,
27 } = ReactTypeOfWork;
28 -var { callCallbacks } = require('ReactFiberUpdateQueue');
28 +var { commitUpdateQueue } = require('ReactFiberUpdateQueue');
29
30 var {
31 Placement,
32 Update,
33 - Callback,
33 ContentReset,
34 } = require('ReactTypeOfSideEffect');
35
@@ -418,25 +417,16 @@ module.exports = function<T, P, I, TI, C, CX>(
417 }
418 attachRef(current, finishedWork, instance);
419 }
421 - // Clear updates from current fiber.
422 - if (finishedWork.alternate) {
423 - finishedWork.alternate.updateQueue = null;
424 - }
425 - if (finishedWork.effectTag & Callback) {
426 - if (finishedWork.callbackList) {
427 - const callbackList = finishedWork.callbackList;
428 - finishedWork.callbackList = null;
429 - callCallbacks(callbackList, instance);
430 - }
420 + if (finishedWork.updateQueue) {
421 + commitUpdateQueue(finishedWork, finishedWork.updateQueue, instance);
422 }
423 return;
424 }
425 case HostRoot: {
435 - const rootFiber = finishedWork.stateNode;
436 - if (rootFiber.callbackList) {
437 - const callbackList = rootFiber.callbackList;
438 - rootFiber.callbackList = null;
439 - callCallbacks(callbackList, rootFiber.current.child.stateNode);
426 + const updateQueue = finishedWork.updateQueue;
427 + if (updateQueue) {
428 + const instance = finishedWork.child && finishedWork.child.stateNode;
429 + commitUpdateQueue(finishedWork, updateQueue, instance);
430 }
431 return;
432 }
src/renderers/shared/fiber/ReactFiberCompleteWork.js
+2 -26
@@ -41,7 +41,6 @@ var {
41 } = ReactTypeOfWork;
42 var {
43 Update,
44 - Callback,
44 } = ReactTypeOfSideEffect;
45
46 if (__DEV__) {
@@ -73,11 +72,6 @@ module.exports = function<T, P, I, TI, C, CX>(
72 workInProgress.effectTag |= Update;
73 }
74
76 - function markCallback(workInProgress : Fiber) {
77 - // Tag the fiber with a callback effect.
78 - workInProgress.effectTag |= Callback;
79 - }
80 -
75 function appendAllYields(yields : Array<ReifiedYield>, workInProgress : Fiber) {
76 let node = workInProgress.child;
77 while (node) {
@@ -187,26 +181,11 @@ module.exports = function<T, P, I, TI, C, CX>(
181 // Don't use the state queue to compute the memoized state. We already
182 // merged it and assigned it to the instance. Transfer it from there.
183 // Also need to transfer the props, because pendingProps will be null
190 - // in the case of an update
184 + // in the case of an update.
185 const { state, props } = workInProgress.stateNode;
192 - const updateQueue = workInProgress.updateQueue;
186 workInProgress.memoizedState = state;
187 workInProgress.memoizedProps = props;
195 - if (current) {
196 - if (current.memoizedProps !== workInProgress.memoizedProps ||
197 - current.memoizedState !== workInProgress.memoizedState ||
198 - updateQueue && updateQueue.isForced) {
199 - markUpdate(workInProgress);
200 - }
201 - } else {
202 - markUpdate(workInProgress);
203 - }
204 - if (updateQueue && updateQueue.hasCallback) {
205 - // Transfer update queue to callbackList field so callbacks can be
206 - // called during commit phase.
207 - workInProgress.callbackList = updateQueue;
208 - markCallback(workInProgress);
209 - }
188 +
189 return null;
190 case HostRoot: {
191 workInProgress.memoizedProps = workInProgress.pendingProps;
@@ -215,9 +194,6 @@ module.exports = function<T, P, I, TI, C, CX>(
194 fiberRoot.context = fiberRoot.pendingContext;
195 fiberRoot.pendingContext = null;
196 }
218 - // TODO: Only mark this as an update if we have any pending callbacks
219 - // on it.
220 - markUpdate(workInProgress);
197 return null;
198 }
199 case HostComponent:
src/renderers/shared/fiber/ReactFiberReconciler.js
+34 -21
@@ -24,7 +24,7 @@ var {
24 var { createFiberRoot } = require('ReactFiberRoot');
25 var ReactFiberScheduler = require('ReactFiberScheduler');
26
27 -var { createUpdateQueue, addCallbackToQueue } = require('ReactFiberUpdateQueue');
27 +var { createUpdateQueue, addCallback } = require('ReactFiberUpdateQueue');
28
29 if (__DEV__) {
30 var ReactFiberInstrumentation = require('ReactFiberInstrumentation');
@@ -109,16 +109,23 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
109 mountContainer(element : ReactElement<any>, containerInfo : C, parentComponent : ?ReactComponent<any, any, any>, callback: ?Function) : OpaqueNode {
110 const context = getContextForSubtree(parentComponent);
111 const root = createFiberRoot(containerInfo, context);
112 - const container = root.current;
113 - if (callback) {
114 - const queue = createUpdateQueue(null);
115 - addCallbackToQueue(queue, callback);
116 - root.callbackList = queue;
117 - }
118 - // TODO: Use pending work/state instead of props.
112 + const current = root.current;
113 +
114 + // TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps.
115 // TODO: This should not override the pendingWorkPriority if there is
116 // higher priority work in the subtree.
121 - container.pendingProps = element;
117 + current.pendingProps = element;
118 + if (current.alternate) {
119 + current.alternate.pendingProps = element;
120 + }
121 + if (callback) {
122 + const queue = current.updateQueue || createUpdateQueue();
123 + addCallback(queue, callback);
124 + current.updateQueue = queue;
125 + if (current.alternate) {
126 + current.alternate.updateQueue = queue;
127 + }
128 + }
129
130 scheduleWork(root);
131
@@ -129,24 +136,30 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
136 // It may seem strange that we don't return the root here, but that will
137 // allow us to have containers that are in the middle of the tree instead
138 // of being roots.
132 - return container;
139 + return current;
140 },
141
142 updateContainer(element : ReactElement<any>, container : OpaqueNode, parentComponent : ?ReactComponent<any, any, any>, callback: ?Function) : void {
143 // TODO: If this is a nested container, this won't be the root.
144 const root : FiberRoot = (container.stateNode : any);
138 - if (callback) {
139 - const queue = root.callbackList ?
140 - root.callbackList :
141 - createUpdateQueue(null);
142 - addCallbackToQueue(queue, callback);
143 - root.callbackList = queue;
144 - }
145 + const current = root.current;
146 +
147 root.pendingContext = getContextForSubtree(parentComponent);
146 - // TODO: Use pending work/state instead of props.
147 - root.current.pendingProps = element;
148 - if (root.current.alternate) {
149 - root.current.alternate.pendingProps = element;
148 +
149 + // TODO: Use the updateQueue and scheduleUpdate, instead of pendingProps.
150 + // TODO: This should not override the pendingWorkPriority if there is
151 + // higher priority work in the subtree.
152 + current.pendingProps = element;
153 + if (current.alternate) {
154 + current.alternate.pendingProps = element;
155 + }
156 + if (callback) {
157 + const queue = current.updateQueue || createUpdateQueue();
158 + addCallback(queue, callback);
159 + current.updateQueue = queue;
160 + if (current.alternate) {
161 + current.alternate.updateQueue = queue;
162 + }
163 }
164
165 scheduleWork(root);
src/renderers/shared/fiber/ReactFiberRoot.js
-3
@@ -13,7 +13,6 @@
13 'use strict';
14
15 import type { Fiber } from 'ReactFiber';
16 -import type { UpdateQueue } from 'ReactFiberUpdateQueue';
16
17 const { createHostRootFiber } = require('ReactFiber');
18
@@ -26,8 +25,6 @@ export type FiberRoot = {
25 isScheduled: boolean,
26 // The work schedule is a linked list.
27 nextScheduledRoot: ?FiberRoot,
29 - // Linked list of callbacks to call after updates are committed.
30 - callbackList: ?UpdateQueue,
28 // Top context object, used by renderSubtreeIntoContainer
29 context: Object,
30 pendingContext: ?Object,
src/renderers/shared/fiber/ReactFiberScheduler.js
-1
@@ -392,7 +392,6 @@ module.exports = function<T, P, I, TI, C, CX>(config : HostConfig<T, P, I, TI, C
392 // The work is now done. We don't need this anymore. This flags
393 // to the system not to redo any work here.
394 workInProgress.pendingProps = null;
395 - workInProgress.updateQueue = null;
395
396 const returnFiber = workInProgress.return;
397 const siblingFiber = workInProgress.sibling;
src/renderers/shared/fiber/ReactFiberUpdateQueue.js
+215 -63
@@ -12,100 +12,252 @@
12
13 'use strict';
14
15 -type UpdateQueueNode = {
16 - partialState: any,
17 - callback: ?Function,
15 +import type { Fiber } from 'ReactFiber';
16 +
17 +type PartialState<State, Props> =
18 + $Subtype<State> |
19 + (prevState: State, props: Props) => $Subtype<State>;
20 +
21 +type Callback = () => void;
22 +
23 +type Update = {
24 + partialState: PartialState<any, any>,
25 + callback: Callback | null,
26 isReplace: boolean,
19 - next: ?UpdateQueueNode,
27 + isForced: boolean,
28 + next: Update | null,
29 };
30
22 -export type UpdateQueue = UpdateQueueNode & {
23 - isForced: boolean,
24 - hasUpdate: boolean,
31 +export type UpdateQueue = {
32 + // Points to the first (oldest) update.
33 + first: Update | null,
34 + // Points to the first pending update. A pending update is one that is not
35 + // part of the progressed work. This could be null even in a non-empty queue,
36 + // when none of the updates are empty.
37 + firstPendingUpdate: Update | null,
38 + // Points to the last (newest) update.
39 + last: Update | null,
40 +
41 + // Used to implement forceUpdate. Only true if there's a merged (non-pending)
42 + // force update; pending force updates do not affect this.
43 + hasForceUpdate: boolean,
44 +
45 + // TODO: Remove this by scheduling the side-effect during the begin phase.
46 hasCallback: boolean,
26 - tail: UpdateQueueNode
47 };
48
29 -exports.createUpdateQueue = function(partialState : mixed) : UpdateQueue {
30 - const queue = {
49 +exports.createUpdateQueue = function() : UpdateQueue {
50 + return {
51 + first: null,
52 + firstPendingUpdate: null,
53 + last: null,
54 +
55 + hasForceUpdate: false,
56 + hasCallback: false,
57 + };
58 +};
59 +
60 +function insertUpdateIntoQueue(queue : UpdateQueue, update : Update) : void {
61 + // Add a pending update to the end of the queue.
62 + // TODO: Once updates have priorities, they should be inserted in the
63 + // correct order. Addtionally, replaceState should remove any pending updates
64 + // that have lower priority from queue.
65 + if (!queue.last) {
66 + // The queue is empty.
67 + queue.first = queue.last = queue.firstPendingUpdate = update;
68 + } else {
69 + // The queue is not empty. Append the update to the end.
70 + queue.last.next = update;
71 + queue.last = update;
72 +
73 + if (!queue.firstPendingUpdate) {
74 + // This is the first pending update. Update the pointer.
75 + queue.firstPendingUpdate = update;
76 + }
77 + }
78 +}
79 +
80 +exports.addUpdate = function(queue : UpdateQueue, partialState : PartialState<any, any> | null) : void {
81 + const update = {
82 partialState,
83 callback: null,
84 isReplace: false,
85 + isForced: false,
86 next: null,
87 + };
88 + insertUpdateIntoQueue(queue, update);
89 +};
90 +
91 +exports.addReplaceUpdate = function(queue : UpdateQueue, state : any | null) : void {
92 + const replaceUpdate = {
93 + partialState: state,
94 + callback: null,
95 + isReplace: true,
96 isForced: false,
36 - hasUpdate: partialState != null,
37 - hasCallback: false,
38 - tail: (null : any),
97 + next: null,
98 };
40 - queue.tail = queue;
41 - return queue;
99 +
100 +
101 + if (!queue.last) {
102 + // The queue is empty.
103 + queue.first = queue.last = queue.firstPendingUpdate = replaceUpdate;
104 + } else {
105 + // The queue is not empty.
106 +
107 + // Drop all existing pending updates.
108 + // TODO: Only drop updates with matching priority.
109 + let lastMergedUpdate = null;
110 + if (queue.firstPendingUpdate) {
111 + let node = queue.first;
112 + while (node && node.next !== queue.firstPendingUpdate) {
113 + node = node.next;
114 + }
115 + lastMergedUpdate = node;
116 + } else {
117 + lastMergedUpdate = queue.last;
118 + }
119 +
120 + if (lastMergedUpdate) {
121 + // Append the new update to the end of the list.
122 + // $FlowFixMe: Union bug (I think? Getting "object literal - This type incompatible with null")
123 + lastMergedUpdate.next = replaceUpdate;
124 + queue.firstPendingUpdate = replaceUpdate;
125 + queue.last = replaceUpdate;
126 + } else {
127 + // Drop everything
128 + queue.first = queue.firstPendingUpdate = queue.last = replaceUpdate;
129 + }
130 + }
131 +
132 };
133
44 -function addToQueue(queue : UpdateQueue, partialState : mixed) : UpdateQueue {
45 - const node = {
46 - partialState,
134 +exports.addForceUpdate = function(queue : UpdateQueue) : void {
135 + const update = {
136 + partialState: null,
137 callback: null,
138 isReplace: false,
139 + isForced: true,
140 next: null,
141 };
51 - queue.tail.next = node;
52 - queue.tail = node;
53 - queue.hasUpdate = queue.hasUpdate || (partialState != null);
54 - return queue;
55 -}
142 + insertUpdateIntoQueue(queue, update);
143 +};
144
57 -exports.addToQueue = addToQueue;
145
59 -exports.addCallbackToQueue = function(queue : UpdateQueue, callback: Function) : UpdateQueue {
60 - if (queue.tail.callback) {
61 - // If the tail already as a callback, add an empty node to queue
62 - addToQueue(queue, null);
146 +exports.addCallback = function(queue : UpdateQueue, callback: Callback) : void {
147 + if (queue.firstPendingUpdate && queue.last && !queue.last.callback) {
148 + // If pending updates already exist, and the last pending update does not
149 + // have a callback, we can add the new callback to that update.
150 + // TODO: Add an additional check to ensure the priority matches.
151 + queue.last.callback = callback;
152 + return;
153 }
64 - queue.tail.callback = callback;
65 - queue.hasCallback = true;
66 - return queue;
154 +
155 + const update = {
156 + partialState: null,
157 + callback,
158 + isReplace: false,
159 + isForced: false,
160 + next: null,
161 + };
162 + insertUpdateIntoQueue(queue, update);
163 };
164
69 -exports.callCallbacks = function(queue : UpdateQueue, context : any) {
70 - let node : ?UpdateQueueNode = queue;
71 - while (node) {
72 - const callback = node.callback;
73 - if (callback) {
74 - if (typeof context !== 'undefined') {
75 - callback.call(context);
76 - } else {
77 - callback();
78 - }
79 - }
80 - node = node.next;
81 - }
165 +exports.hasPendingUpdate = function(queue : UpdateQueue) : boolean {
166 + // TODO: Check priority level
167 + return queue.firstPendingUpdate !== null;
168 };
169
84 -function getStateFromNode(node, instance, state, props) {
85 - if (typeof node.partialState === 'function') {
86 - const updateFn = node.partialState;
87 - return updateFn.call(instance, state, props);
170 +function getStateFromUpdate(update, instance, prevState, props) {
171 + const partialState = update.partialState;
172 + if (typeof partialState === 'function') {
173 + const updateFn = partialState;
174 + return updateFn.call(instance, prevState, props);
175 } else {
89 - return node.partialState;
176 + return partialState;
177 }
178 }
179
93 -exports.mergeUpdateQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any {
94 - let node : ?UpdateQueueNode = queue;
95 - if (queue.isReplace) {
96 - // replaceState is always first in the queue.
97 - prevState = getStateFromNode(queue, instance, prevState, props);
98 - node = queue.next;
99 - if (!node) {
100 - // If there is no more work, we replace the raw object instead of cloning.
101 - return prevState;
180 +// TODO: Move callback effect scheduling here. Rename to beginUpdateQueue or similar.
181 +exports.mergeQueue = function(queue : UpdateQueue, instance : any, prevState : any, props : any) : any {
182 + // This merges the entire update queue into a single object, not just the
183 + // pending updates, because the previous state and props may have changed.
184 + // TODO: Would memoization be worth it?
185 +
186 + // Reset these flags. We'll update them while looping through the queue.
187 + queue.hasForceUpdate = false;
188 + queue.hasCallback = false;
189 +
190 + let state = prevState;
191 + let dontMutatePrevState = true;
192 + let update : Update | null = queue.first;
193 + let isEmpty = true;
194 +
195 + // TODO: Stop merging once we reach an update whose priority doesn't match.
196 + // Should this also apply to updates that were previous merged but bailed out?
197 + while (update) {
198 + let partialState;
199 + if (update.isReplace) {
200 + // A replace should drop all previous updates in the queue, so
201 + // use the original `prevState`, not the accumulated `state`
202 + state = getStateFromUpdate(update, instance, prevState, props);
203 + dontMutatePrevState = true;
204 + isEmpty = false;
205 + } else {
206 + partialState = getStateFromUpdate(update, instance, state, props);
207 + if (partialState) {
208 + if (dontMutatePrevState) {
209 + state = Object.assign({}, state, partialState);
210 + } else {
211 + state = Object.assign(state, partialState);
212 + }
213 + dontMutatePrevState = false;
214 + isEmpty = false;
215 + }
216 + }
217 + if (update.isForced) {
218 + queue.hasForceUpdate = true;
219 + }
220 + if (update.callback) {
221 + queue.hasCallback = true;
222 }
223 + update = update.next;
224 }
104 - let state = Object.assign({}, prevState);
105 - while (node) {
106 - let partialState = getStateFromNode(node, instance, state, props);
107 - Object.assign(state, partialState);
108 - node = node.next;
225 +
226 + // The next pending update is the one that we exited on in the loop above.
227 + // Until priorities are implemented, this is always null.
228 + queue.firstPendingUpdate = update;
229 +
230 + if (isEmpty) {
231 + // None of the updates contained state. Return the original state object.
232 + return prevState;
233 }
234 +
235 return state;
236 };
237 +
238 +exports.commitUpdateQueue = function(finishedWork : Fiber, queue : UpdateQueue, context : mixed) {
239 + if (queue.hasCallback) {
240 + // Call the callbacks on all the non-pending updates.
241 + let update = queue.first;
242 + while (update && update !== queue.firstPendingUpdate) {
243 + const callback = update.callback;
244 + if (typeof callback === 'function') {
245 + callback.call(context);
246 + }
247 + update = update.next;
248 + }
249 + }
250 +
251 + // Drop all completed updates, leaving only the pending updates.
252 + queue.first = queue.firstPendingUpdate;
253 + if (!queue.first) {
254 + // If the list is now empty, we can remove it from the finished work
255 + finishedWork.updateQueue = null;
256 + if (finishedWork.alternate) {
257 + // Normally we don't mutate the current tree, but we do for updates.
258 + // The queue on the work in progress is always the same as the queue
259 + // on the current.
260 + finishedWork.alternate.updateQueue = null;
261 + }
262 + }
263 +};