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
+};