20
const {ClassComponent, HostRoot} = require('ReactTypeOfWork');
21
22
const invariant = require('fbjs/lib/invariant');
23
+
24
if (__DEV__) {
25
var warning = require('fbjs/lib/warning');
26
}
32
// Callbacks are not validated until invocation
33
type Callback = mixed;
34
34
-export type Update = {
35
+export type Update<State> = {
36
expirationTime: ExpirationTime,
37
partialState: PartialState<any, any>,
38
callback: Callback | null,
39
isReplace: boolean,
40
isForced: boolean,
40
- isTopLevelUnmount: boolean,
41
- next: Update | null,
41
+ next: Update<State> | null,
42
};
43
44
// Singly linked-list of updates. When an update is scheduled, it is added to
52
// The work-in-progress queue is always a subset of the current queue.
53
//
54
// When the tree is committed, the work-in-progress becomes the current.
55
-export type UpdateQueue = {
56
- first: Update | null,
57
- last: Update | null,
55
+export type UpdateQueue<State> = {
56
+ // A processed update is not removed from the queue if there are any
57
+ // unprocessed updates that came before it. In that case, we need to keep
58
+ // track of the base state, which represents the base state of the first
59
+ // unprocessed update, which is the same as the first update in the list.
60
+ baseState: State,
61
+ // For the same reason, we keep track of the remaining expiration time.
62
+ expirationTime: ExpirationTime,
63
+ first: Update<State> | null,
64
+ last: Update<State> | null,
65
+ callbackList: Array<Update<State>> | null,
66
hasForceUpdate: boolean,
59
- callbackList: null | Array<Callback>,
67
68
// Dev only
69
isProcessing?: boolean,
70
};
71
65
-let _queue1;
66
-let _queue2;
67
-
68
-function createUpdateQueue(): UpdateQueue {
69
- const queue: UpdateQueue = {
72
+function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
73
+ const queue: UpdateQueue<State> = {
74
+ baseState,
75
+ expirationTime: NoWork,
76
first: null,
77
last: null,
72
- hasForceUpdate: false,
78
callbackList: null,
79
+ hasForceUpdate: false,
80
};
81
if (__DEV__) {
82
queue.isProcessing = false;
84
return queue;
85
}
86
81
-function cloneUpdate(update: Update): Update {
82
- return {
83
- expirationTime: update.expirationTime,
84
- partialState: update.partialState,
85
- callback: update.callback,
86
- isReplace: update.isReplace,
87
- isForced: update.isForced,
88
- isTopLevelUnmount: update.isTopLevelUnmount,
89
- next: null,
90
- };
91
-}
92
-
93
-function insertUpdateIntoQueue(
94
- queue: UpdateQueue,
95
- update: Update,
96
- insertAfter: Update | null,
97
- insertBefore: Update | null,
98
-) {
99
- if (insertAfter !== null) {
100
- insertAfter.next = update;
101
- } else {
102
- // This is the first item in the queue.
103
- update.next = queue.first;
104
- queue.first = update;
105
- }
106
-
107
- if (insertBefore !== null) {
108
- update.next = insertBefore;
87
+function insertUpdateIntoQueue<State>(
88
+ queue: UpdateQueue<State>,
89
+ update: Update<State>,
90
+): void {
91
+ // Append the update to the end of the list.
92
+ if (queue.last === null) {
93
+ // Queue is empty
94
+ queue.first = queue.last = update;
95
} else {
110
- // This is the last item in the queue.
96
+ queue.last.next = update;
97
queue.last = update;
98
}
113
-}
114
-
115
-// Returns the update after which the incoming update should be inserted into
116
-// the queue, or null if it should be inserted at beginning.
117
-function findInsertionPosition(queue, update): Update | null {
118
- const expirationTime = update.expirationTime;
119
- let insertAfter = null;
120
- let insertBefore = null;
121
- if (queue.last !== null && queue.last.expirationTime <= expirationTime) {
122
- // Fast path for the common case where the update should be inserted at
123
- // the end of the queue.
124
- insertAfter = queue.last;
125
- } else {
126
- insertBefore = queue.first;
127
- while (
128
- insertBefore !== null &&
129
- insertBefore.expirationTime <= expirationTime
130
- ) {
131
- insertAfter = insertBefore;
132
- insertBefore = insertBefore.next;
133
- }
99
+ if (
100
+ queue.expirationTime === NoWork ||
101
+ queue.expirationTime > update.expirationTime
102
+ ) {
103
+ queue.expirationTime = update.expirationTime;
104
}
135
- return insertAfter;
105
}
106
+exports.insertUpdateIntoQueue = insertUpdateIntoQueue;
107
138
-function ensureUpdateQueues(fiber: Fiber) {
108
+function insertUpdateIntoFiber<State>(
109
+ fiber: Fiber,
110
+ update: Update<State>,
111
+): void {
112
+ // We'll have at least one and at most two distinct update queues.
113
const alternateFiber = fiber.alternate;
140
-
114
let queue1 = fiber.updateQueue;
115
if (queue1 === null) {
143
- queue1 = fiber.updateQueue = createUpdateQueue();
116
+ queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
117
}
118
119
let queue2;
120
if (alternateFiber !== null) {
121
queue2 = alternateFiber.updateQueue;
122
if (queue2 === null) {
150
- queue2 = alternateFiber.updateQueue = createUpdateQueue();
123
+ queue2 = alternateFiber.updateQueue = createUpdateQueue(
124
+ alternateFiber.memoizedState,
125
+ );
126
}
127
} else {
128
queue2 = null;
129
}
155
-
156
- _queue1 = queue1;
157
- // Return null if there is no alternate queue, or if its queue is the same.
158
- _queue2 = queue2 !== queue1 ? queue2 : null;
159
-}
160
-
161
-// The work-in-progress queue is a subset of the current queue (if it exists).
162
-// We need to insert the incoming update into both lists. However, it's possible
163
-// that the correct position in one list will be different from the position in
164
-// the other. Consider the following case:
165
-//
166
-// Current: 3-5-6
167
-// Work-in-progress: 6
168
-//
169
-// Then we receive an update with priority 4 and insert it into each list:
170
-//
171
-// Current: 3-4-5-6
172
-// Work-in-progress: 4-6
173
-//
174
-// In the current queue, the new update's `next` pointer points to the update
175
-// with priority 5. But in the work-in-progress queue, the pointer points to the
176
-// update with priority 6. Because these two queues share the same persistent
177
-// data structure, this won't do. (This can only happen when the incoming update
178
-// has higher priority than all the updates in the work-in-progress queue.)
179
-//
180
-// To solve this, in the case where the incoming update needs to be inserted
181
-// into two different positions, we'll make a clone of the update and insert
182
-// each copy into a separate queue. This forks the list while maintaining a
183
-// persistent structure, because the update that is added to the work-in-progress
184
-// is always added to the front of the list.
185
-//
186
-// However, if incoming update is inserted into the same position of both lists,
187
-// we shouldn't make a copy.
188
-//
189
-// If the update is cloned, it returns the cloned update.
190
-function insertUpdate(fiber: Fiber, update: Update): Update | null {
191
- // We'll have at least one and at most two distinct update queues.
192
- ensureUpdateQueues(fiber);
193
- const queue1 = _queue1;
194
- const queue2 = _queue2;
130
+ queue2 = queue2 !== queue1 ? queue2 : null;
131
132
// Warn if an update is scheduled from inside an updater function.
133
if (__DEV__) {
142
}
143
}
144
209
- // Find the insertion position in the first queue.
210
- const insertAfter1 = findInsertionPosition(queue1, update);
211
- const insertBefore1 = insertAfter1 !== null
212
- ? insertAfter1.next
213
- : queue1.first;
214
-
145
+ // If there's only one queue, add the update to that queue and exit.
146
if (queue2 === null) {
216
- // If there's no alternate queue, there's nothing else to do but insert.
217
- insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
218
- return null;
147
+ insertUpdateIntoQueue(queue1, update);
148
+ return;
149
}
150
221
- // If there is an alternate queue, find the insertion position.
222
- const insertAfter2 = findInsertionPosition(queue2, update);
223
- const insertBefore2 = insertAfter2 !== null
224
- ? insertAfter2.next
225
- : queue2.first;
226
-
227
- // Now we can insert into the first queue. This must come after finding both
228
- // insertion positions because it mutates the list.
229
- insertUpdateIntoQueue(queue1, update, insertAfter1, insertBefore1);
230
-
231
- // See if the insertion positions are equal. Be careful to only compare
232
- // non-null values.
233
- if (
234
- (insertBefore1 === insertBefore2 && insertBefore1 !== null) ||
235
- (insertAfter1 === insertAfter2 && insertAfter1 !== null)
236
- ) {
237
- // The insertion positions are the same, so when we inserted into the first
238
- // queue, it also inserted into the alternate. All we need to do is update
239
- // the alternate queue's `first` and `last` pointers, in case they
240
- // have changed.
241
- if (insertAfter2 === null) {
242
- queue2.first = update;
243
- }
244
- if (insertBefore2 === null) {
245
- queue2.last = null;
246
- }
247
- return null;
248
- } else {
249
- // The insertion positions are different, so we need to clone the update and
250
- // insert the clone into the alternate queue.
251
- const update2 = cloneUpdate(update);
252
- insertUpdateIntoQueue(queue2, update2, insertAfter2, insertBefore2);
253
- return update2;
151
+ // If either queue is empty, we need to add to both queues.
152
+ if (queue1.last === null || queue2.last === null) {
153
+ insertUpdateIntoQueue(queue1, update);
154
+ insertUpdateIntoQueue(queue2, update);
155
+ return;
156
}
255
-}
256
-
257
-function addUpdate(
258
- fiber: Fiber,
259
- partialState: PartialState<any, any> | null,
260
- callback: mixed,
261
- expirationTime: ExpirationTime,
262
-): void {
263
- const update = {
264
- expirationTime,
265
- partialState,
266
- callback,
267
- isReplace: false,
268
- isForced: false,
269
- isTopLevelUnmount: false,
270
- next: null,
271
- };
272
- insertUpdate(fiber, update);
273
-}
274
-exports.addUpdate = addUpdate;
157
276
-function addReplaceUpdate(
277
- fiber: Fiber,
278
- state: any | null,
279
- callback: Callback | null,
280
- expirationTime: ExpirationTime,
281
-): void {
282
- const update = {
283
- expirationTime,
284
- partialState: state,
285
- callback,
286
- isReplace: true,
287
- isForced: false,
288
- isTopLevelUnmount: false,
289
- next: null,
290
- };
291
- insertUpdate(fiber, update);
158
+ // If both lists are not empty, the last update is the same for both lists
159
+ // because of structural sharing. So, we should only append to one of
160
+ // the lists.
161
+ insertUpdateIntoQueue(queue1, update);
162
+ // But we still need to update the `last` pointer of queue2.
163
+ queue2.last = update;
164
}
293
-exports.addReplaceUpdate = addReplaceUpdate;
294
-
295
-function addForceUpdate(
296
- fiber: Fiber,
297
- callback: Callback | null,
298
- expirationTime: ExpirationTime,
299
-): void {
300
- const update = {
301
- expirationTime,
302
- partialState: null,
303
- callback,
304
- isReplace: false,
305
- isForced: true,
306
- isTopLevelUnmount: false,
307
- next: null,
308
- };
309
- insertUpdate(fiber, update);
310
-}
311
-exports.addForceUpdate = addForceUpdate;
165
+exports.insertUpdateIntoFiber = insertUpdateIntoFiber;
166
167
function getUpdateExpirationTime(fiber: Fiber): ExpirationTime {
314
- const updateQueue = fiber.updateQueue;
315
- if (updateQueue === null) {
168
+ if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
169
return NoWork;
170
}
318
- if (fiber.tag !== ClassComponent && fiber.tag !== HostRoot) {
171
+ const updateQueue = fiber.updateQueue;
172
+ if (updateQueue === null) {
173
return NoWork;
174
}
321
- return updateQueue.first !== null ? updateQueue.first.expirationTime : NoWork;
175
+ return updateQueue.expirationTime;
176
}
177
exports.getUpdateExpirationTime = getUpdateExpirationTime;
178
325
-function addTopLevelUpdate(
326
- fiber: Fiber,
327
- partialState: PartialState<any, any>,
328
- callback: Callback | null,
329
- expirationTime: ExpirationTime,
330
-): void {
331
- const isTopLevelUnmount = partialState.element === null;
332
-
333
- const update = {
334
- expirationTime,
335
- partialState,
336
- callback,
337
- isReplace: false,
338
- isForced: false,
339
- isTopLevelUnmount,
340
- next: null,
341
- };
342
- const update2 = insertUpdate(fiber, update);
343
-
344
- if (isTopLevelUnmount) {
345
- // TODO: Redesign the top-level mount/update/unmount API to avoid this
346
- // special case.
347
- const queue1 = _queue1;
348
- const queue2 = _queue2;
349
-
350
- // Drop all updates that are lower-priority, so that the tree is not
351
- // remounted. We need to do this for both queues.
352
- if (queue1 !== null && update.next !== null) {
353
- update.next = null;
354
- queue1.last = update;
355
- }
356
- if (queue2 !== null && update2 !== null && update2.next !== null) {
357
- update2.next = null;
358
- queue2.last = update;
359
- }
360
- }
361
-}
362
-exports.addTopLevelUpdate = addTopLevelUpdate;
363
-
179
function getStateFromUpdate(update, instance, prevState, props) {
180
const partialState = update.partialState;
181
if (typeof partialState === 'function') {
186
}
187
}
188
374
-function beginUpdateQueue(
189
+function processUpdateQueue<State>(
190
current: Fiber | null,
191
workInProgress: Fiber,
377
- queue: UpdateQueue,
192
+ queue: UpdateQueue<State>,
193
instance: any,
379
- prevState: any,
194
props: any,
195
renderExpirationTime: ExpirationTime,
382
-): any {
196
+): State {
197
if (current !== null && current.updateQueue === queue) {
198
// We need to create a work-in-progress queue, by cloning the current queue.
199
const currentQueue = queue;
200
queue = workInProgress.updateQueue = {
201
+ baseState: currentQueue.baseState,
202
+ expirationTime: currentQueue.expirationTime,
203
first: currentQueue.first,
204
last: currentQueue.last,
205
// These fields are no longer valid because they were already committed.
215
queue.isProcessing = true;
216
}
217
402
- // Calculate these using the the existing values as a base.
403
- let callbackList = queue.callbackList;
404
- let hasForceUpdate = queue.hasForceUpdate;
218
+ // Reset the remaining expiration time. If we skip over any updates, we'll
219
+ // increase this accordingly.
220
+ queue.expirationTime = NoWork;
221
406
- // Applies updates with matching priority to the previous state to create
407
- // a new state object.
408
- let state = prevState;
222
+ let state = queue.baseState;
223
let dontMutatePrevState = true;
224
let update = queue.first;
411
- while (update !== null && update.expirationTime <= renderExpirationTime) {
412
- // Remove each update from the queue right before it is processed. That way
413
- // if setState is called from inside an updater function, the new update
414
- // will be inserted in the correct position.
415
- queue.first = update.next;
416
- if (queue.first === null) {
417
- queue.last = null;
225
+ let didSkip = false;
226
+ while (update !== null) {
227
+ const updateExpirationTime = update.expirationTime;
228
+ if (updateExpirationTime > renderExpirationTime) {
229
+ // This update does not have sufficient priority. Skip it.
230
+ const remainingExpirationTime = queue.expirationTime;
231
+ if (
232
+ remainingExpirationTime === NoWork ||
233
+ remainingExpirationTime > updateExpirationTime
234
+ ) {
235
+ // Update the remaining expiration time.
236
+ queue.expirationTime = updateExpirationTime;
237
+ }
238
+ if (!didSkip) {
239
+ didSkip = true;
240
+ queue.baseState = state;
241
+ }
242
+ // Continue to the next update.
243
+ update = update.next;
244
+ continue;
245
+ }
246
+
247
+ // This update does have sufficient priority.
248
+
249
+ // If no previous updates were skipped, drop this update from the queue by
250
+ // advancing the head of the list.
251
+ if (!didSkip) {
252
+ queue.first = update.next;
253
+ if (queue.first === null) {
254
+ queue.last = null;
255
+ }
256
}
257
258
+ // Process the update
259
let partialState;
260
if (update.isReplace) {
261
state = getStateFromUpdate(update, instance, state, props);
264
partialState = getStateFromUpdate(update, instance, state, props);
265
if (partialState) {
266
if (dontMutatePrevState) {
267
+ // $FlowFixMe: Idk how to type this properly.
268
state = Object.assign({}, state, partialState);
269
} else {
270
state = Object.assign(state, partialState);
273
}
274
}
275
if (update.isForced) {
436
- hasForceUpdate = true;
276
+ queue.hasForceUpdate = true;
277
}
438
- // Second condition ignores top-level unmount callbacks if they are not the
439
- // last update in the queue, since a subsequent update will cause a remount.
440
- if (
441
- update.callback !== null &&
442
- !(update.isTopLevelUnmount && update.next !== null)
443
- ) {
444
- callbackList = callbackList !== null ? callbackList : [];
445
- callbackList.push(update.callback);
446
- workInProgress.effectTag |= CallbackEffect;
278
+ if (update.callback !== null) {
279
+ // Append to list of callbacks.
280
+ let callbackList = queue.callbackList;
281
+ if (callbackList === null) {
282
+ callbackList = queue.callbackList = [];
283
+ }
284
+ callbackList.push(update);
285
}
286
update = update.next;
287
}
288
451
- queue.callbackList = callbackList;
452
- queue.hasForceUpdate = hasForceUpdate;
453
-
454
- if (queue.first === null && callbackList === null && !hasForceUpdate) {
455
- // The queue is empty and there are no callbacks. We can reset it.
289
+ if (queue.callbackList !== null) {
290
+ workInProgress.effectTag |= CallbackEffect;
291
+ } else if (queue.first === null && !queue.hasForceUpdate) {
292
+ // The queue is empty. We can reset it.
293
workInProgress.updateQueue = null;
294
}
295
296
+ if (!didSkip) {
297
+ didSkip = true;
298
+ queue.baseState = state;
299
+ }
300
+
301
if (__DEV__) {
302
// No longer processing.
303
queue.isProcessing = false;
305
306
return state;
307
}
466
-exports.beginUpdateQueue = beginUpdateQueue;
308
+exports.processUpdateQueue = processUpdateQueue;
309
468
-function commitCallbacks(
469
- finishedWork: Fiber,
470
- queue: UpdateQueue,
471
- context: mixed,
472
-) {
310
+function commitCallbacks<State>(queue: UpdateQueue<State>, context: any) {
311
const callbackList = queue.callbackList;
312
if (callbackList === null) {
313
return;
314
}
477
-
315
// Set the list to null to make sure they don't get called more than once.
316
queue.callbackList = null;
480
-
317
for (let i = 0; i < callbackList.length; i++) {
482
- const callback = callbackList[i];
318
+ const update = callbackList[i];
319
+ const callback = update.callback;
320
+ // This update might be processed again. Clear the callback so it's only
321
+ // called once.
322
+ update.callback = null;
323
invariant(
324
typeof callback === 'function',
325
'Invalid argument passed as callback. Expected a function. Instead ' +