115
payload: any,
116
callback: (() => mixed) | null,
117
118
- next: Update<State>,
118
+ next: Update<State> | null,
119
120
// DEV only
121
priority?: ReactPriorityLevel,
125
126
export type UpdateQueue<State> = {|
127
baseState: State,
128
- baseQueue: Update<State> | null,
128
+ firstBaseUpdate: Update<State> | null,
129
+ lastBaseUpdate: Update<State> | null,
130
shared: SharedQueue<State>,
131
effects: Array<Update<State>> | null,
132
|};
155
export function initializeUpdateQueue<State>(fiber: Fiber): void {
156
const queue: UpdateQueue<State> = {
157
baseState: fiber.memoizedState,
157
- baseQueue: null,
158
+ firstBaseUpdate: null,
159
+ lastBaseUpdate: null,
160
shared: {
161
pending: null,
162
},
175
if (queue === currentQueue) {
176
const clone: UpdateQueue<State> = {
177
baseState: currentQueue.baseState,
176
- baseQueue: currentQueue.baseQueue,
178
+ firstBaseUpdate: currentQueue.firstBaseUpdate,
179
+ lastBaseUpdate: currentQueue.lastBaseUpdate,
180
shared: currentQueue.shared,
181
effects: currentQueue.effects,
182
};
196
payload: null,
197
callback: null,
198
196
- next: (null: any),
199
+ next: null,
200
};
198
- update.next = update;
201
if (__DEV__) {
202
update.priority = getCurrentPriorityLevel();
203
}
240
241
export function enqueueCapturedUpdate<State>(
242
workInProgress: Fiber,
241
- update: Update<State>,
243
+ capturedUpdate: Update<State>,
244
) {
245
+ // Captured updates are updates that are thrown by a child during the render
246
+ // phase. They should be discarded if the render is aborted. Therefore,
247
+ // we should only put them on the work-in-progress queue, not the current one.
248
+ let queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
249
+
250
+ // Check if the work-in-progress queue is a clone.
251
const current = workInProgress.alternate;
252
if (current !== null) {
245
- // Ensure the work-in-progress queue is a clone
246
- cloneUpdateQueue(current, workInProgress);
253
+ const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
254
+ if (queue === currentQueue) {
255
+ // The work-in-progress queue is the same as current. This happens when
256
+ // we bail out on a parent fiber that then captures an error thrown by
257
+ // a child. Since we want to append the update only to the work-in
258
+ // -progress queue, we need to clone the updates. We usually clone during
259
+ // processUpdateQueue, but that didn't happen in this case because we
260
+ // skipped over the parent when we bailed out.
261
+ let newFirst = null;
262
+ let newLast = null;
263
+ const firstBaseUpdate = queue.firstBaseUpdate;
264
+ if (firstBaseUpdate !== null) {
265
+ // Loop through the updates and clone them.
266
+ let update = firstBaseUpdate;
267
+ do {
268
+ const clone: Update<State> = {
269
+ expirationTime: update.expirationTime,
270
+ suspenseConfig: update.suspenseConfig,
271
+
272
+ tag: update.tag,
273
+ payload: update.payload,
274
+ callback: update.callback,
275
+
276
+ next: null,
277
+ };
278
+ if (newLast === null) {
279
+ newFirst = newLast = clone;
280
+ } else {
281
+ newLast.next = clone;
282
+ newLast = clone;
283
+ }
284
+ update = update.next;
285
+ } while (update !== null);
286
+
287
+ // Append the captured update the end of the cloned list.
288
+ if (newLast === null) {
289
+ newFirst = newLast = capturedUpdate;
290
+ } else {
291
+ newLast.next = capturedUpdate;
292
+ newLast = capturedUpdate;
293
+ }
294
+ } else {
295
+ // There are no base updates.
296
+ newFirst = newLast = capturedUpdate;
297
+ }
298
+ queue = {
299
+ baseState: currentQueue.baseState,
300
+ firstBaseUpdate: newFirst,
301
+ lastBaseUpdate: newLast,
302
+ shared: currentQueue.shared,
303
+ effects: currentQueue.effects,
304
+ };
305
+ workInProgress.updateQueue = queue;
306
+ return;
307
+ }
308
}
309
249
- // Captured updates go only on the work-in-progress queue.
250
- const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
310
// Append the update to the end of the list.
252
- const last = queue.baseQueue;
253
- if (last === null) {
254
- queue.baseQueue = update.next = update;
255
- update.next = update;
311
+ const lastBaseUpdate = queue.lastBaseUpdate;
312
+ if (lastBaseUpdate === null) {
313
+ queue.firstBaseUpdate = capturedUpdate;
314
} else {
257
- update.next = last.next;
258
- last.next = update;
315
+ lastBaseUpdate.next = capturedUpdate;
316
}
317
+ queue.lastBaseUpdate = capturedUpdate;
318
}
319
320
function getStateFromUpdate<State>(
405
currentlyProcessingQueue = queue.shared;
406
}
407
350
- // The last rebase update that is NOT part of the base state.
351
- let baseQueue = queue.baseQueue;
408
+ let firstBaseUpdate = queue.firstBaseUpdate;
409
+ let lastBaseUpdate = queue.lastBaseUpdate;
410
353
- // The last pending update that hasn't been processed yet.
411
+ // Check if there are pending updates. If so, transfer them to the base queue.
412
let pendingQueue = queue.shared.pending;
413
if (pendingQueue !== null) {
356
- // We have new updates that haven't been processed yet.
357
- // We'll add them to the base queue.
358
- if (baseQueue !== null) {
359
- // Merge the pending queue and the base queue.
360
- let baseFirst = baseQueue.next;
361
- let pendingFirst = pendingQueue.next;
362
- baseQueue.next = pendingFirst;
363
- pendingQueue.next = baseFirst;
364
- }
414
+ queue.shared.pending = null;
415
366
- baseQueue = pendingQueue;
416
+ // The pending queue is circular. Disconnect the pointer between first
417
+ // and last so that it's non-circular.
418
+ const lastPendingUpdate = pendingQueue;
419
+ const firstPendingUpdate = lastPendingUpdate.next;
420
+ lastPendingUpdate.next = null;
421
+ // Append pending updates to base queue
422
+ if (lastBaseUpdate === null) {
423
+ firstBaseUpdate = firstPendingUpdate;
424
+ } else {
425
+ lastBaseUpdate.next = firstPendingUpdate;
426
+ }
427
+ lastBaseUpdate = lastPendingUpdate;
428
368
- queue.shared.pending = null;
429
+ // If there's a current queue, and it's different from the base queue, then
430
+ // we need to transfer the updates to that queue, too. Because the base
431
+ // queue is a singly-linked list with no cycles, we can append to both
432
+ // lists and take advantage of structural sharing.
433
// TODO: Pass `current` as argument
434
const current = workInProgress.alternate;
435
if (current !== null) {
372
- const currentQueue = current.updateQueue;
373
- if (currentQueue !== null) {
374
- currentQueue.baseQueue = pendingQueue;
436
+ // This is always non-null on a ClassComponent or HostRoot
437
+ const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
438
+ const currentLastBaseUpdate = currentQueue.lastBaseUpdate;
439
+ if (currentLastBaseUpdate !== lastBaseUpdate) {
440
+ if (currentLastBaseUpdate === null) {
441
+ currentQueue.firstBaseUpdate = firstPendingUpdate;
442
+ } else {
443
+ currentLastBaseUpdate.next = firstPendingUpdate;
444
+ }
445
+ currentQueue.lastBaseUpdate = lastPendingUpdate;
446
}
447
}
448
}
449
450
// These values may change as we process the queue.
380
- if (baseQueue !== null) {
381
- let first = baseQueue.next;
451
+ if (firstBaseUpdate !== null) {
452
// Iterate through the list of updates to compute the result.
453
let newState = queue.baseState;
454
let newExpirationTime = NoWork;
455
456
let newBaseState = null;
387
- let newBaseQueueFirst = null;
388
- let newBaseQueueLast = null;
389
-
390
- if (first !== null) {
391
- let update = first;
392
- do {
393
- const updateExpirationTime = update.expirationTime;
394
- if (updateExpirationTime < renderExpirationTime) {
395
- // Priority is insufficient. Skip this update. If this is the first
396
- // skipped update, the previous update/state is the new base
397
- // update/state.
457
+ let newFirstBaseUpdate = null;
458
+ let newLastBaseUpdate = null;
459
+
460
+ let update = firstBaseUpdate;
461
+ do {
462
+ const updateExpirationTime = update.expirationTime;
463
+ if (updateExpirationTime < renderExpirationTime) {
464
+ // Priority is insufficient. Skip this update. If this is the first
465
+ // skipped update, the previous update/state is the new base
466
+ // update/state.
467
+ const clone: Update<State> = {
468
+ expirationTime: update.expirationTime,
469
+ suspenseConfig: update.suspenseConfig,
470
+
471
+ tag: update.tag,
472
+ payload: update.payload,
473
+ callback: update.callback,
474
+
475
+ next: null,
476
+ };
477
+ if (newLastBaseUpdate === null) {
478
+ newFirstBaseUpdate = newLastBaseUpdate = clone;
479
+ newBaseState = newState;
480
+ } else {
481
+ newLastBaseUpdate = newLastBaseUpdate.next = clone;
482
+ }
483
+ // Update the remaining priority in the queue.
484
+ if (updateExpirationTime > newExpirationTime) {
485
+ newExpirationTime = updateExpirationTime;
486
+ }
487
+ } else {
488
+ // This update does have sufficient priority.
489
+
490
+ if (newLastBaseUpdate !== null) {
491
const clone: Update<State> = {
399
- expirationTime: update.expirationTime,
492
+ expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
493
suspenseConfig: update.suspenseConfig,
494
495
tag: update.tag,
496
payload: update.payload,
497
callback: update.callback,
498
406
- next: (null: any),
499
+ next: null,
500
};
408
- if (newBaseQueueLast === null) {
409
- newBaseQueueFirst = newBaseQueueLast = clone;
410
- newBaseState = newState;
411
- } else {
412
- newBaseQueueLast = newBaseQueueLast.next = clone;
413
- }
414
- // Update the remaining priority in the queue.
415
- if (updateExpirationTime > newExpirationTime) {
416
- newExpirationTime = updateExpirationTime;
417
- }
418
- } else {
419
- // This update does have sufficient priority.
420
-
421
- if (newBaseQueueLast !== null) {
422
- const clone: Update<State> = {
423
- expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
424
- suspenseConfig: update.suspenseConfig,
425
-
426
- tag: update.tag,
427
- payload: update.payload,
428
- callback: update.callback,
429
-
430
- next: (null: any),
431
- };
432
- newBaseQueueLast = newBaseQueueLast.next = clone;
433
- }
434
-
435
- // Mark the event time of this update as relevant to this render pass.
436
- // TODO: This should ideally use the true event time of this update rather than
437
- // its priority which is a derived and not reverseable value.
438
- // TODO: We should skip this update if it was already committed but currently
439
- // we have no way of detecting the difference between a committed and suspended
440
- // update here.
441
- markRenderEventTimeAndConfig(
442
- updateExpirationTime,
443
- update.suspenseConfig,
444
- );
445
-
446
- // Process this update.
447
- newState = getStateFromUpdate(
448
- workInProgress,
449
- queue,
450
- update,
451
- newState,
452
- props,
453
- instance,
454
- );
455
- const callback = update.callback;
456
- if (callback !== null) {
457
- workInProgress.effectTag |= Callback;
458
- let effects = queue.effects;
459
- if (effects === null) {
460
- queue.effects = [update];
461
- } else {
462
- effects.push(update);
463
- }
464
- }
501
+ newLastBaseUpdate = newLastBaseUpdate.next = clone;
502
}
466
- update = update.next;
467
- if (update === null || update === first) {
468
- pendingQueue = queue.shared.pending;
469
- if (pendingQueue === null) {
470
- break;
503
+
504
+ // Mark the event time of this update as relevant to this render pass.
505
+ // TODO: This should ideally use the true event time of this update rather than
506
+ // its priority which is a derived and not reverseable value.
507
+ // TODO: We should skip this update if it was already committed but currently
508
+ // we have no way of detecting the difference between a committed and suspended
509
+ // update here.
510
+ markRenderEventTimeAndConfig(
511
+ updateExpirationTime,
512
+ update.suspenseConfig,
513
+ );
514
+
515
+ // Process this update.
516
+ newState = getStateFromUpdate(
517
+ workInProgress,
518
+ queue,
519
+ update,
520
+ newState,
521
+ props,
522
+ instance,
523
+ );
524
+ const callback = update.callback;
525
+ if (callback !== null) {
526
+ workInProgress.effectTag |= Callback;
527
+ let effects = queue.effects;
528
+ if (effects === null) {
529
+ queue.effects = [update];
530
} else {
472
- // An update was scheduled from inside a reducer. Add the new
473
- // pending updates to the end of the list and keep processing.
474
- update = baseQueue.next = pendingQueue.next;
475
- pendingQueue.next = first;
476
- queue.baseQueue = baseQueue = pendingQueue;
477
- queue.shared.pending = null;
531
+ effects.push(update);
532
}
533
}
480
- } while (true);
481
- }
534
+ }
535
+ update = update.next;
536
+ if (update === null) {
537
+ pendingQueue = queue.shared.pending;
538
+ if (pendingQueue === null) {
539
+ break;
540
+ } else {
541
+ // An update was scheduled from inside a reducer. Add the new
542
+ // pending updates to the end of the list and keep processing.
543
+ const lastPendingUpdate = pendingQueue;
544
+ // Intentionally unsound. Pending updates form a circular list, but we
545
+ // unravel them when transferring them to the base queue.
546
+ const firstPendingUpdate = ((lastPendingUpdate.next: any): Update<State>);
547
+ lastPendingUpdate.next = null;
548
+ update = firstPendingUpdate;
549
+ queue.lastBaseUpdate = lastPendingUpdate;
550
+ queue.shared.pending = null;
551
+ }
552
+ }
553
+ } while (true);
554
483
- if (newBaseQueueLast === null) {
555
+ if (newLastBaseUpdate === null) {
556
newBaseState = newState;
485
- } else {
486
- newBaseQueueLast.next = (newBaseQueueFirst: any);
557
}
558
559
queue.baseState = ((newBaseState: any): State);
490
- queue.baseQueue = newBaseQueueLast;
560
+ queue.firstBaseUpdate = newFirstBaseUpdate;
561
+ queue.lastBaseUpdate = newLastBaseUpdate;
562
563
// Set the remaining expiration time to be whatever is remaining in the queue.
564
// This should be fine because the only two other things that contribute to