89
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
90
import type {ReactPriorityLevel} from './SchedulerWithReactIntegration';
91
92
-import {NoWork} from './ReactFiberExpirationTime';
92
+import {NoWork, Sync} from './ReactFiberExpirationTime';
93
import {
94
enterDisallowedContextReadInDEV,
95
exitDisallowedContextReadInDEV,
96
} from './ReactFiberNewContext';
97
import {Callback, ShouldCapture, DidCapture} from 'shared/ReactSideEffectTags';
98
-import {ClassComponent} from 'shared/ReactWorkTags';
98
99
import {debugRenderPhaseSideEffectsForStrictMode} from 'shared/ReactFeatureFlags';
100
116
payload: any,
117
callback: (() => mixed) | null,
118
120
- next: Update<State> | null,
121
- nextEffect: Update<State> | null,
119
+ next: Update<State>,
120
121
//DEV only
122
priority?: ReactPriorityLevel,
123
};
124
125
+type SharedQueue<State> = {
126
+ pending: Update<State> | null,
127
+};
128
+
129
export type UpdateQueue<State> = {
130
baseState: State,
129
-
130
- firstUpdate: Update<State> | null,
131
- lastUpdate: Update<State> | null,
132
-
133
- firstCapturedUpdate: Update<State> | null,
134
- lastCapturedUpdate: Update<State> | null,
135
-
136
- firstEffect: Update<State> | null,
137
- lastEffect: Update<State> | null,
138
-
139
- firstCapturedEffect: Update<State> | null,
140
- lastCapturedEffect: Update<State> | null,
131
+ baseQueue: Update<State> | null,
132
+ shared: SharedQueue<State>,
133
+ effects: Array<Update<State>> | null,
134
};
135
136
export const UpdateState = 0;
154
};
155
}
156
164
-export function createUpdateQueue<State>(baseState: State): UpdateQueue<State> {
157
+export function initializeUpdateQueue<State>(fiber: Fiber): void {
158
const queue: UpdateQueue<State> = {
166
- baseState,
167
- firstUpdate: null,
168
- lastUpdate: null,
169
- firstCapturedUpdate: null,
170
- lastCapturedUpdate: null,
171
- firstEffect: null,
172
- lastEffect: null,
173
- firstCapturedEffect: null,
174
- lastCapturedEffect: null,
159
+ baseState: fiber.memoizedState,
160
+ baseQueue: null,
161
+ shared: {
162
+ pending: null,
163
+ },
164
+ effects: null,
165
};
176
- return queue;
166
+ fiber.updateQueue = queue;
167
}
168
179
-function cloneUpdateQueue<State>(
180
- currentQueue: UpdateQueue<State>,
181
-): UpdateQueue<State> {
182
- const queue: UpdateQueue<State> = {
183
- baseState: currentQueue.baseState,
184
- firstUpdate: currentQueue.firstUpdate,
185
- lastUpdate: currentQueue.lastUpdate,
186
-
187
- // TODO: With resuming, if we bail out and resuse the child tree, we should
188
- // keep these effects.
189
- firstCapturedUpdate: null,
190
- lastCapturedUpdate: null,
191
-
192
- firstEffect: null,
193
- lastEffect: null,
194
-
195
- firstCapturedEffect: null,
196
- lastCapturedEffect: null,
197
- };
198
- return queue;
169
+export function cloneUpdateQueue<State>(
170
+ current: Fiber,
171
+ workInProgress: Fiber,
172
+): void {
173
+ // Clone the update queue from current. Unless it's already a clone.
174
+ const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
175
+ const currentQueue: UpdateQueue<State> = (current.updateQueue: any);
176
+ if (queue === currentQueue) {
177
+ const clone: UpdateQueue<State> = {
178
+ baseState: currentQueue.baseState,
179
+ baseQueue: currentQueue.baseQueue,
180
+ shared: currentQueue.shared,
181
+ effects: currentQueue.effects,
182
+ };
183
+ workInProgress.updateQueue = clone;
184
+ }
185
}
186
187
export function createUpdate(
196
payload: null,
197
callback: null,
198
213
- next: null,
214
- nextEffect: null,
199
+ next: (null: any),
200
};
201
+ update.next = update;
202
if (__DEV__) {
203
update.priority = getCurrentPriorityLevel();
204
}
205
return update;
206
}
207
222
-function appendUpdateToQueue<State>(
223
- queue: UpdateQueue<State>,
224
- update: Update<State>,
225
-) {
226
- // Append the update to the end of the list.
227
- if (queue.lastUpdate === null) {
228
- // Queue is empty
229
- queue.firstUpdate = queue.lastUpdate = update;
230
- } else {
231
- queue.lastUpdate.next = update;
232
- queue.lastUpdate = update;
233
- }
234
-}
235
-
208
export function enqueueUpdate<State>(fiber: Fiber, update: Update<State>) {
237
- // Update queues are created lazily.
238
- const alternate = fiber.alternate;
239
- let queue1;
240
- let queue2;
241
- if (alternate === null) {
242
- // There's only one fiber.
243
- queue1 = fiber.updateQueue;
244
- queue2 = null;
245
- if (queue1 === null) {
246
- queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
247
- }
248
- } else {
249
- // There are two owners.
250
- queue1 = fiber.updateQueue;
251
- queue2 = alternate.updateQueue;
252
- if (queue1 === null) {
253
- if (queue2 === null) {
254
- // Neither fiber has an update queue. Create new ones.
255
- queue1 = fiber.updateQueue = createUpdateQueue(fiber.memoizedState);
256
- queue2 = alternate.updateQueue = createUpdateQueue(
257
- alternate.memoizedState,
258
- );
259
- } else {
260
- // Only one fiber has an update queue. Clone to create a new one.
261
- queue1 = fiber.updateQueue = cloneUpdateQueue(queue2);
262
- }
263
- } else {
264
- if (queue2 === null) {
265
- // Only one fiber has an update queue. Clone to create a new one.
266
- queue2 = alternate.updateQueue = cloneUpdateQueue(queue1);
267
- } else {
268
- // Both owners have an update queue.
269
- }
270
- }
209
+ const updateQueue = fiber.updateQueue;
210
+ if (updateQueue === null) {
211
+ // Only occurs if the fiber has been unmounted.
212
+ return;
213
}
272
- if (queue2 === null || queue1 === queue2) {
273
- // There's only a single queue.
274
- appendUpdateToQueue(queue1, update);
214
+
215
+ const sharedQueue = updateQueue.shared;
216
+ const pending = sharedQueue.pending;
217
+ if (pending === null) {
218
+ // This is the first update. Create a circular list.
219
+ update.next = update;
220
} else {
276
- // There are two queues. We need to append the update to both queues,
277
- // while accounting for the persistent structure of the list — we don't
278
- // want the same update to be added multiple times.
279
- if (queue1.lastUpdate === null || queue2.lastUpdate === null) {
280
- // One of the queues is not empty. We must add the update to both queues.
281
- appendUpdateToQueue(queue1, update);
282
- appendUpdateToQueue(queue2, update);
283
- } else {
284
- // Both queues are non-empty. The last update is the same in both lists,
285
- // because of structural sharing. So, only append to one of the lists.
286
- appendUpdateToQueue(queue1, update);
287
- // But we still need to update the `lastUpdate` pointer of queue2.
288
- queue2.lastUpdate = update;
289
- }
221
+ update.next = pending.next;
222
+ pending.next = update;
223
}
224
+ sharedQueue.pending = update;
225
226
if (__DEV__) {
227
if (
294
- fiber.tag === ClassComponent &&
295
- (currentlyProcessingQueue === queue1 ||
296
- (queue2 !== null && currentlyProcessingQueue === queue2)) &&
228
+ currentlyProcessingQueue === sharedQueue &&
229
!didWarnUpdateInsideUpdate
230
) {
231
warningWithoutStack(
244
workInProgress: Fiber,
245
update: Update<State>,
246
) {
315
- // Captured updates go into a separate list, and only on the work-in-
316
- // progress queue.
317
- let workInProgressQueue = workInProgress.updateQueue;
318
- if (workInProgressQueue === null) {
319
- workInProgressQueue = workInProgress.updateQueue = createUpdateQueue(
320
- workInProgress.memoizedState,
321
- );
322
- } else {
323
- // TODO: I put this here rather than createWorkInProgress so that we don't
324
- // clone the queue unnecessarily. There's probably a better way to
325
- // structure this.
326
- workInProgressQueue = ensureWorkInProgressQueueIsAClone(
327
- workInProgress,
328
- workInProgressQueue,
329
- );
247
+ const current = workInProgress.alternate;
248
+ if (current !== null) {
249
+ // Ensure the work-in-progress queue is a clone
250
+ cloneUpdateQueue(current, workInProgress);
251
}
252
253
+ // Captured updates go only on the work-in-progress queue.
254
+ const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
255
// Append the update to the end of the list.
333
- if (workInProgressQueue.lastCapturedUpdate === null) {
334
- // This is the first render phase update
335
- workInProgressQueue.firstCapturedUpdate = workInProgressQueue.lastCapturedUpdate = update;
256
+ const last = queue.baseQueue;
257
+ if (last === null) {
258
+ queue.baseQueue = update.next = update;
259
+ update.next = update;
260
} else {
337
- workInProgressQueue.lastCapturedUpdate.next = update;
338
- workInProgressQueue.lastCapturedUpdate = update;
261
+ update.next = last.next;
262
+ last.next = update;
263
}
264
}
265
342
-function ensureWorkInProgressQueueIsAClone<State>(
343
- workInProgress: Fiber,
344
- queue: UpdateQueue<State>,
345
-): UpdateQueue<State> {
346
- const current = workInProgress.alternate;
347
- if (current !== null) {
348
- // If the work-in-progress queue is equal to the current queue,
349
- // we need to clone it first.
350
- if (queue === current.updateQueue) {
351
- queue = workInProgress.updateQueue = cloneUpdateQueue(queue);
352
- }
353
- }
354
- return queue;
355
-}
356
-
266
function getStateFromUpdate<State>(
267
workInProgress: Fiber,
268
queue: UpdateQueue<State>,
338
339
export function processUpdateQueue<State>(
340
workInProgress: Fiber,
432
- queue: UpdateQueue<State>,
341
props: any,
342
instance: any,
343
renderExpirationTime: ExpirationTime,
344
): void {
437
- hasForceUpdate = false;
345
+ // This is always non-null on a ClassComponent or HostRoot
346
+ const queue: UpdateQueue<State> = (workInProgress.updateQueue: any);
347
439
- queue = ensureWorkInProgressQueueIsAClone(workInProgress, queue);
348
+ hasForceUpdate = false;
349
350
if (__DEV__) {
442
- currentlyProcessingQueue = queue;
351
+ currentlyProcessingQueue = queue.shared;
352
}
353
445
- // These values may change as we process the queue.
446
- let newBaseState = queue.baseState;
447
- let newFirstUpdate = null;
448
- let newExpirationTime = NoWork;
449
-
450
- // Iterate through the list of updates to compute the result.
451
- let update = queue.firstUpdate;
452
- let resultState = newBaseState;
453
- while (update !== null) {
454
- const updateExpirationTime = update.expirationTime;
455
- if (updateExpirationTime < renderExpirationTime) {
456
- // This update does not have sufficient priority. Skip it.
457
- if (newFirstUpdate === null) {
458
- // This is the first skipped update. It will be the first update in
459
- // the new list.
460
- newFirstUpdate = update;
461
- // Since this is the first update that was skipped, the current result
462
- // is the new base state.
463
- newBaseState = resultState;
464
- }
465
- // Since this update will remain in the list, update the remaining
466
- // expiration time.
467
- if (newExpirationTime < updateExpirationTime) {
468
- newExpirationTime = updateExpirationTime;
469
- }
470
- } else {
471
- // This update does have sufficient priority.
472
-
473
- // Mark the event time of this update as relevant to this render pass.
474
- // TODO: This should ideally use the true event time of this update rather than
475
- // its priority which is a derived and not reverseable value.
476
- // TODO: We should skip this update if it was already committed but currently
477
- // we have no way of detecting the difference between a committed and suspended
478
- // update here.
479
- markRenderEventTimeAndConfig(updateExpirationTime, update.suspenseConfig);
480
-
481
- // Process it and compute a new result.
482
- resultState = getStateFromUpdate(
483
- workInProgress,
484
- queue,
485
- update,
486
- resultState,
487
- props,
488
- instance,
489
- );
490
- const callback = update.callback;
491
- if (callback !== null) {
492
- workInProgress.effectTag |= Callback;
493
- // Set this to null, in case it was mutated during an aborted render.
494
- update.nextEffect = null;
495
- if (queue.lastEffect === null) {
496
- queue.firstEffect = queue.lastEffect = update;
497
- } else {
498
- queue.lastEffect.nextEffect = update;
499
- queue.lastEffect = update;
500
- }
354
+ // The last rebase update that is NOT part of the base state.
355
+ let baseQueue = queue.baseQueue;
356
+
357
+ // The last pending update that hasn't been processed yet.
358
+ let pendingQueue = queue.shared.pending;
359
+ if (pendingQueue !== null) {
360
+ // We have new updates that haven't been processed yet.
361
+ // We'll add them to the base queue.
362
+ if (baseQueue !== null) {
363
+ // Merge the pending queue and the base queue.
364
+ let baseFirst = baseQueue.next;
365
+ let pendingFirst = pendingQueue.next;
366
+ baseQueue.next = pendingFirst;
367
+ pendingQueue.next = baseFirst;
368
+ }
369
+
370
+ baseQueue = pendingQueue;
371
+
372
+ queue.shared.pending = null;
373
+ // TODO: Pass `current` as argument
374
+ const current = workInProgress.alternate;
375
+ if (current !== null) {
376
+ const currentQueue = current.updateQueue;
377
+ if (currentQueue !== null) {
378
+ currentQueue.baseQueue = pendingQueue;
379
}
380
}
503
- // Continue to the next update.
504
- update = update.next;
381
}
382
507
- // Separately, iterate though the list of captured updates.
508
- let newFirstCapturedUpdate = null;
509
- update = queue.firstCapturedUpdate;
510
- while (update !== null) {
511
- const updateExpirationTime = update.expirationTime;
512
- if (updateExpirationTime < renderExpirationTime) {
513
- // This update does not have sufficient priority. Skip it.
514
- if (newFirstCapturedUpdate === null) {
515
- // This is the first skipped captured update. It will be the first
516
- // update in the new list.
517
- newFirstCapturedUpdate = update;
518
- // If this is the first update that was skipped, the current result is
519
- // the new base state.
520
- if (newFirstUpdate === null) {
521
- newBaseState = resultState;
522
- }
523
- }
524
- // Since this update will remain in the list, update the remaining
525
- // expiration time.
526
- if (newExpirationTime < updateExpirationTime) {
527
- newExpirationTime = updateExpirationTime;
528
- }
529
- } else {
530
- // This update does have sufficient priority. Process it and compute
531
- // a new result.
532
- resultState = getStateFromUpdate(
533
- workInProgress,
534
- queue,
535
- update,
536
- resultState,
537
- props,
538
- instance,
539
- );
540
- const callback = update.callback;
541
- if (callback !== null) {
542
- workInProgress.effectTag |= Callback;
543
- // Set this to null, in case it was mutated during an aborted render.
544
- update.nextEffect = null;
545
- if (queue.lastCapturedEffect === null) {
546
- queue.firstCapturedEffect = queue.lastCapturedEffect = update;
383
+ // These values may change as we process the queue.
384
+ if (baseQueue !== null) {
385
+ let first = baseQueue.next;
386
+ // Iterate through the list of updates to compute the result.
387
+ let newState = queue.baseState;
388
+ let newExpirationTime = NoWork;
389
+
390
+ let newBaseState = null;
391
+ let newBaseQueueFirst = null;
392
+ let newBaseQueueLast = null;
393
+
394
+ if (first !== null) {
395
+ let update = first;
396
+ do {
397
+ const updateExpirationTime = update.expirationTime;
398
+ if (updateExpirationTime < renderExpirationTime) {
399
+ // Priority is insufficient. Skip this update. If this is the first
400
+ // skipped update, the previous update/state is the new base
401
+ // update/state.
402
+ const clone: Update<State> = {
403
+ expirationTime: update.expirationTime,
404
+ suspenseConfig: update.suspenseConfig,
405
+
406
+ tag: update.tag,
407
+ payload: update.payload,
408
+ callback: update.callback,
409
+
410
+ next: (null: any),
411
+ };
412
+ if (newBaseQueueLast === null) {
413
+ newBaseQueueFirst = newBaseQueueLast = clone;
414
+ newBaseState = newState;
415
+ } else {
416
+ newBaseQueueLast = newBaseQueueLast.next = clone;
417
+ }
418
+ // Update the remaining priority in the queue.
419
+ if (updateExpirationTime > newExpirationTime) {
420
+ newExpirationTime = updateExpirationTime;
421
+ }
422
} else {
548
- queue.lastCapturedEffect.nextEffect = update;
549
- queue.lastCapturedEffect = update;
423
+ // This update does have sufficient priority.
424
+
425
+ if (newBaseQueueLast !== null) {
426
+ const clone: Update<State> = {
427
+ expirationTime: Sync, // This update is going to be committed so we never want uncommit it.
428
+ suspenseConfig: update.suspenseConfig,
429
+
430
+ tag: update.tag,
431
+ payload: update.payload,
432
+ callback: update.callback,
433
+
434
+ next: (null: any),
435
+ };
436
+ newBaseQueueLast = newBaseQueueLast.next = clone;
437
+ }
438
+
439
+ // Mark the event time of this update as relevant to this render pass.
440
+ // TODO: This should ideally use the true event time of this update rather than
441
+ // its priority which is a derived and not reverseable value.
442
+ // TODO: We should skip this update if it was already committed but currently
443
+ // we have no way of detecting the difference between a committed and suspended
444
+ // update here.
445
+ markRenderEventTimeAndConfig(
446
+ updateExpirationTime,
447
+ update.suspenseConfig,
448
+ );
449
+
450
+ // Process this update.
451
+ newState = getStateFromUpdate(
452
+ workInProgress,
453
+ queue,
454
+ update,
455
+ newState,
456
+ props,
457
+ instance,
458
+ );
459
+ const callback = update.callback;
460
+ if (callback !== null) {
461
+ workInProgress.effectTag |= Callback;
462
+ let effects = queue.effects;
463
+ if (effects === null) {
464
+ queue.effects = [update];
465
+ } else {
466
+ effects.push(update);
467
+ }
468
+ }
469
}
551
- }
470
+ update = update.next;
471
+ if (update === null || update === first) {
472
+ pendingQueue = queue.shared.pending;
473
+ if (pendingQueue === null) {
474
+ break;
475
+ } else {
476
+ // An update was scheduled from inside a reducer. Add the new
477
+ // pending updates to the end of the list and keep processing.
478
+ update = baseQueue.next = pendingQueue.next;
479
+ pendingQueue.next = first;
480
+ queue.baseQueue = baseQueue = pendingQueue;
481
+ queue.shared.pending = null;
482
+ }
483
+ }
484
+ } while (true);
485
}
553
- update = update.next;
554
- }
486
556
- if (newFirstUpdate === null) {
557
- queue.lastUpdate = null;
558
- }
559
- if (newFirstCapturedUpdate === null) {
560
- queue.lastCapturedUpdate = null;
561
- } else {
562
- workInProgress.effectTag |= Callback;
563
- }
564
- if (newFirstUpdate === null && newFirstCapturedUpdate === null) {
565
- // We processed every update, without skipping. That means the new base
566
- // state is the same as the result state.
567
- newBaseState = resultState;
568
- }
487
+ if (newBaseQueueLast === null) {
488
+ newBaseState = newState;
489
+ } else {
490
+ newBaseQueueLast.next = (newBaseQueueFirst: any);
491
+ }
492
570
- queue.baseState = newBaseState;
571
- queue.firstUpdate = newFirstUpdate;
572
- queue.firstCapturedUpdate = newFirstCapturedUpdate;
573
-
574
- // Set the remaining expiration time to be whatever is remaining in the queue.
575
- // This should be fine because the only two other things that contribute to
576
- // expiration time are props and context. We're already in the middle of the
577
- // begin phase by the time we start processing the queue, so we've already
578
- // dealt with the props. Context in components that specify
579
- // shouldComponentUpdate is tricky; but we'll have to account for
580
- // that regardless.
581
- markUnprocessedUpdateTime(newExpirationTime);
582
- workInProgress.expirationTime = newExpirationTime;
583
- workInProgress.memoizedState = resultState;
493
+ queue.baseState = ((newBaseState: any): State);
494
+ queue.baseQueue = newBaseQueueLast;
495
+
496
+ // Set the remaining expiration time to be whatever is remaining in the queue.
497
+ // This should be fine because the only two other things that contribute to
498
+ // expiration time are props and context. We're already in the middle of the
499
+ // begin phase by the time we start processing the queue, so we've already
500
+ // dealt with the props. Context in components that specify
501
+ // shouldComponentUpdate is tricky; but we'll have to account for
502
+ // that regardless.
503
+ markUnprocessedUpdateTime(newExpirationTime);
504
+ workInProgress.expirationTime = newExpirationTime;
505
+ workInProgress.memoizedState = newState;
506
+ }
507
508
if (__DEV__) {
509
currentlyProcessingQueue = null;
534
instance: any,
535
renderExpirationTime: ExpirationTime,
536
): void {
614
- // If the finished render included captured updates, and there are still
615
- // lower priority updates left over, we need to keep the captured updates
616
- // in the queue so that they are rebased and not dropped once we process the
617
- // queue again at the lower priority.
618
- if (finishedQueue.firstCapturedUpdate !== null) {
619
- // Join the captured update list to the end of the normal list.
620
- if (finishedQueue.lastUpdate !== null) {
621
- finishedQueue.lastUpdate.next = finishedQueue.firstCapturedUpdate;
622
- finishedQueue.lastUpdate = finishedQueue.lastCapturedUpdate;
623
- }
624
- // Clear the list of captured updates.
625
- finishedQueue.firstCapturedUpdate = finishedQueue.lastCapturedUpdate = null;
626
- }
627
-
537
// Commit the effects
629
- commitUpdateEffects(finishedQueue.firstEffect, instance);
630
- finishedQueue.firstEffect = finishedQueue.lastEffect = null;
631
-
632
- commitUpdateEffects(finishedQueue.firstCapturedEffect, instance);
633
- finishedQueue.firstCapturedEffect = finishedQueue.lastCapturedEffect = null;
634
-}
635
-
636
-function commitUpdateEffects<State>(
637
- effect: Update<State> | null,
638
- instance: any,
639
-): void {
640
- while (effect !== null) {
641
- const callback = effect.callback;
642
- if (callback !== null) {
643
- effect.callback = null;
644
- callCallback(callback, instance);
538
+ const effects = finishedQueue.effects;
539
+ finishedQueue.effects = null;
540
+ if (effects !== null) {
541
+ for (let i = 0; i < effects.length; i++) {
542
+ const effect = effects[i];
543
+ const callback = effect.callback;
544
+ if (callback !== null) {
545
+ effect.callback = null;
546
+ callCallback(callback, instance);
547
+ }
548
}
646
- effect = effect.nextEffect;
549
}
550
}