9
10
import type {Thenable, Wakeable} from 'shared/ReactTypes';
11
import type {Fiber, FiberRoot} from './ReactInternalTypes';
12
-import type {ExpirationTime} from './ReactFiberExpirationTime.old';
12
+import type {Lanes, Lane} from './ReactFiberLane';
13
import type {ReactPriorityLevel} from './ReactInternalTypes';
14
import type {Interaction} from 'scheduler/src/Tracing';
15
import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16
import type {SuspenseState} from './ReactFiberSuspenseComponent.old';
17
import type {Effect as HookEffect} from './ReactFiberHooks.old';
18
+import type {StackCursor} from './ReactFiberStack.old';
19
20
import {
21
warnAboutDeprecatedLifecycles,
25
enableProfilerCommitHooks,
26
enableSchedulerTracing,
27
warnAboutUnmockedScheduler,
27
- disableSchedulerTimeoutBasedOnReactExpirationTime,
28
- enableDebugTracing,
28
+ deferRenderPhaseUpdateToNextBatch,
29
} from 'shared/ReactFeatureFlags';
30
import ReactSharedInternals from 'shared/ReactSharedInternals';
31
import invariant from 'shared/invariant';
38
shouldYield,
39
requestPaint,
40
now,
41
- NoPriority,
42
- ImmediatePriority,
43
- UserBlockingPriority,
44
- NormalPriority,
45
- LowPriority,
46
- IdlePriority,
41
+ NoPriority as NoSchedulerPriority,
42
+ ImmediatePriority as ImmediateSchedulerPriority,
43
+ UserBlockingPriority as UserBlockingSchedulerPriority,
44
+ NormalPriority as NormalSchedulerPriority,
45
flushSyncCallbackQueue,
46
scheduleSyncCallback,
47
} from './SchedulerWithReactIntegration.old';
50
-import {
51
- logCommitStarted,
52
- logCommitStopped,
53
- logLayoutEffectsStarted,
54
- logLayoutEffectsStopped,
55
- logPassiveEffectsStarted,
56
- logPassiveEffectsStopped,
57
- logRenderStarted,
58
- logRenderStopped,
59
-} from './DebugTracing';
48
49
// The scheduler is imported here *only* to detect whether it's been mocked
50
import * as Scheduler from 'scheduler';
67
createWorkInProgress,
68
assignFiberPropertiesInDEV,
69
} from './ReactFiber.old';
82
-import {
83
- isRootSuspendedAtTime,
84
- markRootSuspendedAtTime,
85
- markRootFinishedAtTime,
86
- markRootUpdatedAtTime,
87
- markRootExpiredAtTime,
88
-} from './ReactFiberRoot.old';
70
import {
71
NoMode,
72
StrictMode,
85
MemoComponent,
86
SimpleMemoComponent,
87
Block,
88
+ OffscreenComponent,
89
+ LegacyHiddenComponent,
90
} from './ReactWorkTags';
91
import {LegacyRoot} from './ReactRootTags';
92
import {
108
HydratingAndUpdate,
109
} from './ReactSideEffectTags';
110
import {
128
- NoWork,
129
- Sync,
130
- Never,
131
- msToExpirationTime,
132
- expirationTimeToMs,
133
- computeInteractiveExpiration,
134
- computeAsyncExpiration,
135
- computeSuspenseExpiration,
136
- inferPriorityFromExpirationTime,
137
- LOW_PRIORITY_EXPIRATION,
138
- Batched,
139
- Idle,
140
-} from './ReactFiberExpirationTime.old';
111
+ NoLanePriority,
112
+ SyncLanePriority,
113
+ InputDiscreteLanePriority,
114
+ TransitionShortLanePriority,
115
+ TransitionLongLanePriority,
116
+ NoLanes,
117
+ NoLane,
118
+ SyncLane,
119
+ SyncBatchedLane,
120
+ OffscreenLane,
121
+ NoTimestamp,
122
+ findUpdateLane,
123
+ findTransitionLane,
124
+ includesSomeLane,
125
+ isSubsetOfLanes,
126
+ mergeLanes,
127
+ removeLanes,
128
+ pickArbitraryLane,
129
+ hasDiscreteLanes,
130
+ hasUpdatePriority,
131
+ getNextLanes,
132
+ returnNextLanesPriority,
133
+ markStarvedLanesAsExpired,
134
+ getLanesToRetrySynchronouslyOnError,
135
+ markRootUpdated,
136
+ markRootSuspended as markRootSuspended_dontCallThisOneDirectly,
137
+ markRootPinged,
138
+ markRootExpired,
139
+ markDiscreteUpdatesExpired,
140
+ markRootFinished,
141
+ schedulerPriorityToLanePriority,
142
+ lanePriorityToSchedulerPriority,
143
+} from './ReactFiberLane';
144
import {beginWork as originalBeginWork} from './ReactFiberBeginWork.old';
145
import {completeWork} from './ReactFiberCompleteWork.old';
146
import {unwindWork, unwindInterruptedWork} from './ReactFiberUnwindWork.old';
169
getIsUpdatingOpaqueValueInRenderPhaseInDEV,
170
} from './ReactFiberHooks.old';
171
import {createCapturedValue} from './ReactCapturedValue';
172
+import {
173
+ push as pushToStack,
174
+ pop as popFromStack,
175
+ createCursor,
176
+} from './ReactFiberStack.old';
177
178
import {
179
recordCommitTime,
237
let workInProgressRoot: FiberRoot | null = null;
238
// The fiber we're working on
239
let workInProgress: Fiber | null = null;
232
-// The expiration time we're rendering
233
-let renderExpirationTime: ExpirationTime = NoWork;
240
+// The lanes we're rendering
241
+let workInProgressRootRenderLanes: Lanes = NoLanes;
242
+
243
+// Stack that allows components to change the render lanes for its subtree
244
+// This is a superset of the lanes we started working on at the root. The only
245
+// case where it's different from `workInProgressRootRenderLanes` is when we
246
+// enter a subtree that is hidden and needs to be unhidden: Suspense and
247
+// Offscreen component.
248
+//
249
+// Most things in the work loop should deal with workInProgressRootRenderLanes.
250
+// Most things in begin/complete phases should deal with subtreeRenderLanes.
251
+let subtreeRenderLanes: Lanes = NoLanes;
252
+const subtreeRenderLanesCursor: StackCursor<Lanes> = createCursor(NoLanes);
253
+
254
// Whether to root completed, errored, suspended, etc.
255
let workInProgressRootExitStatus: RootExitStatus = RootIncomplete;
256
// A fatal error, if one is thrown
257
let workInProgressRootFatalError: mixed = null;
258
// Most recent event time among processed updates during this render.
239
-// This is conceptually a time stamp but expressed in terms of an ExpirationTime
240
-// because we deal mostly with expiration times in the hot path, so this avoids
241
-// the conversion happening in the hot path.
242
-let workInProgressRootLatestProcessedExpirationTime: ExpirationTime = Sync;
243
-let workInProgressRootLatestSuspenseTimeout: ExpirationTime = Sync;
259
+let workInProgressRootLatestProcessedEventTime: number = NoTimestamp;
260
+let workInProgressRootLatestSuspenseTimeout: number = NoTimestamp;
261
let workInProgressRootCanSuspendUsingConfig: null | SuspenseConfig = null;
262
+// "Included" lanes refer to lanes that were worked on during this render. It's
263
+// slightly different than `renderLanes` because `renderLanes` can change as you
264
+// enter and exit an Offscreen tree. This value is the combination of all render
265
+// lanes for the entire render phase.
266
+let workInProgressRootIncludedLanes: Lanes = NoLanes;
267
// The work left over by components that were visited during this render. Only
268
// includes unprocessed updates, not work in bailed out children.
247
-let workInProgressRootNextUnprocessedUpdateTime: ExpirationTime = NoWork;
269
+let workInProgressRootSkippedLanes: Lanes = NoLanes;
270
+// Lanes that were updated (in an interleaved event) during this render.
271
+let workInProgressRootUpdatedLanes: Lanes = NoLanes;
272
+// Lanes that were pinged (in an interleaved event) during this render.
273
+let workInProgressRootPingedLanes: Lanes = NoLanes;
274
+
275
+let mostRecentlyUpdatedRoot: FiberRoot | null = null;
276
249
-// If we're pinged while rendering we don't always restart immediately.
250
-// This flag determines if it might be worthwhile to restart if an opportunity
251
-// happens latere.
252
-let workInProgressRootHasPendingPing: boolean = false;
277
// The most recent time we committed a fallback. This lets us ensure a train
278
// model where we don't commit new loading states in too quick succession.
279
let globalMostRecentFallbackTime: number = 0;
280
const FALLBACK_THROTTLE_MS: number = 500;
281
+const DEFAULT_TIMEOUT_MS: number = 5000;
282
283
let nextEffect: Fiber | null = null;
284
let hasUncaughtError = false;
287
288
let rootDoesHavePassiveEffects: boolean = false;
289
let rootWithPendingPassiveEffects: FiberRoot | null = null;
265
-let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoPriority;
266
-let pendingPassiveEffectsExpirationTime: ExpirationTime = NoWork;
290
+let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority;
291
+let pendingPassiveEffectsLanes: Lanes = NoLanes;
292
let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
293
let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
294
let pendingPassiveProfilerEffects: Array<Fiber> = [];
295
271
-let rootsWithPendingDiscreteUpdates: Map<
272
- FiberRoot,
273
- ExpirationTime,
274
-> | null = null;
296
+let rootsWithPendingDiscreteUpdates: Set<FiberRoot> | null = null;
297
298
// Use these to prevent an infinite loop of nested updates
299
const NESTED_UPDATE_LIMIT = 50;
303
const NESTED_PASSIVE_UPDATE_LIMIT = 50;
304
let nestedPassiveUpdateCount: number = 0;
305
284
-// Marks the need to reschedule pending interactions at these expiration times
306
+// Marks the need to reschedule pending interactions at these lanes
307
// during the commit phase. This enables them to be traced across components
308
// that spawn new work during render. E.g. hidden boundaries, suspended SSR
309
// hydration or SuspenseList.
288
-let spawnedWorkDuringRender: null | Array<ExpirationTime> = null;
289
-
290
-// Expiration times are computed by adding to the current time (the start
291
-// time). However, if two updates are scheduled within the same event, we
292
-// should treat their start times as simultaneous, even if the actual clock
293
-// time has advanced between the first and second call.
310
+// TODO: Can use a bitmask instead of an array
311
+let spawnedWorkDuringRender: null | Array<Lane | Lanes> = null;
312
295
-// In other words, because expiration times determine how updates are batched,
296
-// we want all updates of like priority that occur within the same event to
297
-// receive the same expiration time. Otherwise we get tearing.
298
-let currentEventTime: ExpirationTime = NoWork;
313
+// If two updates are scheduled within the same event, we should treat their
314
+// event times as simultaneous, even if the actual clock time has advanced
315
+// between the first and second call.
316
+let currentEventTime: number = NoTimestamp;
317
+let currentEventWipLanes: Lanes = NoLanes;
318
+let currentEventPendingLanes: Lanes = NoLanes;
319
320
// Dev only flag that tracks if passive effects are currently being flushed.
321
// We warn about state updates for unmounted components differently in this case.
328
return workInProgressRoot;
329
}
330
311
-export function requestCurrentTimeForUpdate() {
331
+export function requestEventTime() {
332
if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
333
// We're inside React, so it's fine to read the actual time.
314
- return msToExpirationTime(now());
334
+ return now();
335
}
336
// We're not inside React, so we may be in the middle of a browser event.
317
- if (currentEventTime !== NoWork) {
337
+ if (currentEventTime !== NoTimestamp) {
338
// Use the same start time for all updates until we enter React again.
339
return currentEventTime;
340
}
341
// This is the first update since React yielded. Compute a new start time.
322
- currentEventTime = msToExpirationTime(now());
342
+ currentEventTime = now();
343
return currentEventTime;
344
}
345
346
export function getCurrentTime() {
327
- return msToExpirationTime(now());
347
+ return now();
348
}
349
330
-export function computeExpirationForFiber(
331
- currentTime: ExpirationTime,
350
+export function requestUpdateLane(
351
fiber: Fiber,
333
- suspenseConfig: null | SuspenseConfig,
334
-): ExpirationTime {
352
+ suspenseConfig: SuspenseConfig | null,
353
+): Lane {
354
+ // Special cases
355
const mode = fiber.mode;
356
if ((mode & BlockingMode) === NoMode) {
337
- return Sync;
338
- }
339
-
340
- const priorityLevel = getCurrentPriorityLevel();
341
- if ((mode & ConcurrentMode) === NoMode) {
342
- return priorityLevel === ImmediatePriority ? Sync : Batched;
343
- }
344
-
345
- if ((executionContext & RenderContext) !== NoContext) {
346
- // Use whatever time we're already rendering
347
- // TODO: Should there be a way to opt out, like with `runWithPriority`?
348
- return renderExpirationTime;
349
- }
350
-
351
- let expirationTime;
357
+ return (SyncLane: Lane);
358
+ } else if ((mode & ConcurrentMode) === NoMode) {
359
+ return getCurrentPriorityLevel() === ImmediateSchedulerPriority
360
+ ? (SyncLane: Lane)
361
+ : (SyncBatchedLane: Lane);
362
+ } else if (
363
+ !deferRenderPhaseUpdateToNextBatch &&
364
+ (executionContext & RenderContext) !== NoContext &&
365
+ workInProgressRootRenderLanes !== NoLanes
366
+ ) {
367
+ // This is a render phase update. These are not officially supported. The
368
+ // old behavior is to give this the same "thread" (expiration time) as
369
+ // whatever is currently rendering. So if you call `setState` on a component
370
+ // that happens later in the same render, it will flush. Ideally, we want to
371
+ // remove the special case and treat them as if they came from an
372
+ // interleaved event. Regardless, this pattern is not officially supported.
373
+ // This behavior is only a fallback. The flag only exists until we can roll
374
+ // out the setState warnning, since existing code might accidentally rely on
375
+ // the current behavior.
376
+ return pickArbitraryLane(workInProgressRootRenderLanes);
377
+ }
378
+
379
+ // The algorithm for assigning an update to a lane should be stable for all
380
+ // updates at the same priority within the same event. To do this, the inputs
381
+ // to the algorithm must be the same. For example, we use the `renderLanes`
382
+ // to avoid choosing a lane that is already in the middle of rendering.
383
+ //
384
+ // However, the "included" lanes could be mutated in between updates in the
385
+ // same event, like if you perform an update inside `flushSync`. Or any other
386
+ // codepath that might call `prepareFreshStack`.
387
+ //
388
+ // The trick we use is to cache the first of each of these inputs within an
389
+ // event. Then reset the cached values once we can be sure the event is over.
390
+ // Our heuristic for that is whenever we enter a concurrent work loop.
391
+ //
392
+ // We'll do the same for `currentEventPendingLanes` below.
393
+ if (currentEventWipLanes === NoLanes) {
394
+ currentEventWipLanes = workInProgressRootIncludedLanes;
395
+ }
396
+
397
+ let lane;
398
if (suspenseConfig !== null) {
353
- // Compute an expiration time based on the Suspense timeout.
354
- expirationTime = computeSuspenseExpiration(
355
- currentTime,
356
- suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION,
399
+ // Use the size of the timeout as a heuristic to prioritize shorter
400
+ // transitions over longer ones.
401
+ // TODO: This will coerce numbers larger than 31 bits to 0.
402
+ const timeoutMs = suspenseConfig.timeoutMs;
403
+ const transitionLanePriority =
404
+ timeoutMs === undefined || (timeoutMs | 0) < 10000
405
+ ? TransitionShortLanePriority
406
+ : TransitionLongLanePriority;
407
+
408
+ if (currentEventPendingLanes !== NoLanes) {
409
+ currentEventPendingLanes =
410
+ mostRecentlyUpdatedRoot !== null
411
+ ? mostRecentlyUpdatedRoot.pendingLanes
412
+ : NoLanes;
413
+ }
414
+
415
+ lane = findTransitionLane(
416
+ transitionLanePriority,
417
+ currentEventWipLanes,
418
+ currentEventPendingLanes,
419
);
420
} else {
359
- // Compute an expiration time based on the Scheduler priority.
360
- switch (priorityLevel) {
361
- case ImmediatePriority:
362
- expirationTime = Sync;
363
- break;
364
- case UserBlockingPriority:
365
- // TODO: Rename this to computeUserBlockingExpiration
366
- expirationTime = computeInteractiveExpiration(currentTime);
367
- break;
368
- case NormalPriority:
369
- case LowPriority: // TODO: Handle LowPriority
370
- // TODO: Rename this to... something better.
371
- expirationTime = computeAsyncExpiration(currentTime);
372
- break;
373
- case IdlePriority:
374
- expirationTime = Idle;
375
- break;
376
- default:
377
- invariant(false, 'Expected a valid priority level');
378
- }
379
- }
380
-
381
- // If we're in the middle of rendering a tree, do not update at the same
382
- // expiration time that is already rendering.
383
- // TODO: We shouldn't have to do this if the update is on a different root.
384
- // Refactor computeExpirationForFiber + scheduleUpdate so we have access to
385
- // the root when we check for this condition.
386
- if (workInProgressRoot !== null && expirationTime === renderExpirationTime) {
387
- // This is a trick to move this update into a separate batch
388
- expirationTime -= 1;
389
- }
390
-
391
- return expirationTime;
392
-}
393
-
394
-export function priorityLevelToLabel(
395
- priorityLevel: ReactPriorityLevel,
396
-): string {
397
- if (__DEV__ && enableDebugTracing) {
398
- switch (priorityLevel) {
399
- case ImmediatePriority:
400
- return 'immediate';
401
- case UserBlockingPriority:
402
- return 'user-blocking';
403
- case NormalPriority:
404
- return 'normal';
405
- case LowPriority:
406
- return 'low';
407
- case IdlePriority:
408
- return 'idle';
409
- default:
410
- return 'other';
421
+ // TODO: If we're not inside `runWithPriority`, this returns the priority
422
+ // of the currently running task. That's probably not what we want.
423
+ const schedulerPriority = getCurrentPriorityLevel();
424
+
425
+ if (
426
+ // TODO: Temporary. We're removing the concept of discrete updates.
427
+ (executionContext & DiscreteEventContext) !== NoContext &&
428
+ schedulerPriority === UserBlockingSchedulerPriority
429
+ ) {
430
+ lane = findUpdateLane(InputDiscreteLanePriority, currentEventWipLanes);
431
+ } else {
432
+ const lanePriority = schedulerPriorityToLanePriority(schedulerPriority);
433
+ lane = findUpdateLane(lanePriority, currentEventWipLanes);
434
}
412
- } else {
413
- return '';
435
}
436
+
437
+ return lane;
438
}
439
440
export function scheduleUpdateOnFiber(
441
fiber: Fiber,
419
- expirationTime: ExpirationTime,
442
+ lane: Lane,
443
+ eventTime: number,
444
) {
445
checkForNestedUpdates();
446
warnAboutRenderPhaseUpdatesInDEV(fiber);
447
424
- const root = markUpdateTimeFromFiberToRoot(fiber, expirationTime);
448
+ const root = markUpdateLaneFromFiberToRoot(fiber, lane);
449
if (root === null) {
450
warnAboutUpdateOnUnmountedFiberInDEV(fiber);
427
- return;
451
+ return null;
452
}
453
430
- // TODO: computeExpirationForFiber also reads the priority. Pass the
454
+ // TODO: requestUpdateLanePriority also reads the priority. Pass the
455
// priority as an argument to that function and this one.
456
const priorityLevel = getCurrentPriorityLevel();
457
434
- if (expirationTime === Sync) {
458
+ if (lane === SyncLane) {
459
if (
460
// Check if we're inside unbatchedUpdates
461
(executionContext & LegacyUnbatchedContext) !== NoContext &&
463
(executionContext & (RenderContext | CommitContext)) === NoContext
464
) {
465
// Register pending interactions on the root to avoid losing traced interaction data.
442
- schedulePendingInteractions(root, expirationTime);
466
+ schedulePendingInteractions(root, lane);
467
468
// This is a legacy edge case. The initial mount of a ReactDOM.render-ed
469
// root inside of batchedUpdates should be synchronous, but layout updates
470
// should be deferred until the end of the batch.
471
performSyncWorkOnRoot(root);
472
} else {
449
- ensureRootIsScheduled(root);
450
- schedulePendingInteractions(root, expirationTime);
473
+ ensureRootIsScheduled(root, eventTime);
474
+ schedulePendingInteractions(root, lane);
475
if (executionContext === NoContext) {
476
// Flush the synchronous work now, unless we're already working or inside
477
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
487
(executionContext & DiscreteEventContext) !== NoContext &&
488
// Only updates at user-blocking priority or greater are considered
489
// discrete, even inside a discrete event.
466
- (priorityLevel === UserBlockingPriority ||
467
- priorityLevel === ImmediatePriority)
490
+ (priorityLevel === UserBlockingSchedulerPriority ||
491
+ priorityLevel === ImmediateSchedulerPriority)
492
) {
493
// This is the result of a discrete event. Track the lowest priority
494
// discrete update per root so we can flush them early, if needed.
495
if (rootsWithPendingDiscreteUpdates === null) {
472
- rootsWithPendingDiscreteUpdates = new Map([[root, expirationTime]]);
496
+ rootsWithPendingDiscreteUpdates = new Set([root]);
497
} else {
474
- const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
475
- if (
476
- lastDiscreteTime === undefined ||
477
- lastDiscreteTime > expirationTime
478
- ) {
479
- rootsWithPendingDiscreteUpdates.set(root, expirationTime);
480
- }
498
+ rootsWithPendingDiscreteUpdates.add(root);
499
}
500
}
501
// Schedule other updates after in case the callback is sync.
484
- ensureRootIsScheduled(root);
485
- schedulePendingInteractions(root, expirationTime);
502
+ ensureRootIsScheduled(root, eventTime);
503
+ schedulePendingInteractions(root, lane);
504
}
505
+
506
+ // We use this when assigning a lane for a transition inside
507
+ // `requestUpdateLane`. We assume it's the same as the root being updated,
508
+ // since in the common case of a single root app it probably is. If it's not
509
+ // the same root, then it's not a huge deal, we just might batch more stuff
510
+ // together more than necessary.
511
+ mostRecentlyUpdatedRoot = root;
512
}
513
514
// This is split into a separate function so we can mark a fiber with pending
515
// work without treating it as a typical update that originates from an event;
516
// e.g. retrying a Suspense boundary isn't an update, but it does schedule work
517
// on a fiber.
493
-function markUpdateTimeFromFiberToRoot(fiber, expirationTime) {
494
- // Update the source fiber's expiration time
495
- if (fiber.expirationTime < expirationTime) {
496
- fiber.expirationTime = expirationTime;
497
- }
518
+function markUpdateLaneFromFiberToRoot(
519
+ fiber: Fiber,
520
+ lane: Lane,
521
+): FiberRoot | null {
522
+ // Update the source fiber's lanes
523
+ fiber.lanes = mergeLanes(fiber.lanes, lane);
524
let alternate = fiber.alternate;
499
- if (alternate !== null && alternate.expirationTime < expirationTime) {
500
- alternate.expirationTime = expirationTime;
525
+ if (alternate !== null) {
526
+ alternate.lanes = mergeLanes(alternate.lanes, lane);
527
}
528
if (__DEV__) {
529
if (
533
warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
534
}
535
}
510
-
536
// Walk the parent path to the root and update the child expiration time.
537
let node = fiber.return;
538
let root = null;
549
warnAboutUpdateOnNotYetMountedFiberInDEV(fiber);
550
}
551
}
527
- if (node.childExpirationTime < expirationTime) {
528
- node.childExpirationTime = expirationTime;
529
- if (
530
- alternate !== null &&
531
- alternate.childExpirationTime < expirationTime
532
- ) {
533
- alternate.childExpirationTime = expirationTime;
534
- }
535
- } else if (
536
- alternate !== null &&
537
- alternate.childExpirationTime < expirationTime
538
- ) {
539
- alternate.childExpirationTime = expirationTime;
552
+ node.childLanes = mergeLanes(node.childLanes, lane);
553
+ if (alternate !== null) {
554
+ alternate.childLanes = mergeLanes(alternate.childLanes, lane);
555
}
556
if (node.return === null && node.tag === HostRoot) {
557
root = node.stateNode;
562
}
563
564
if (root !== null) {
565
+ // Mark that the root has a pending update.
566
+ markRootUpdated(root, lane);
567
if (workInProgressRoot === root) {
568
// Received an update to a tree that's in the middle of rendering. Mark
552
- // that's unprocessed work on this root.
553
- markUnprocessedUpdateTime(expirationTime);
554
-
569
+ // that there was an interleaved update work on this root. Unless the
570
+ // `deferRenderPhaseUpdateToNextBatch` flag is off and this is a render
571
+ // phase update. In that case, we don't treat render phase updates as if
572
+ // they were interleaved, for backwards compat reasons.
573
+ if (
574
+ deferRenderPhaseUpdateToNextBatch ||
575
+ (executionContext & RenderContext) === NoContext
576
+ ) {
577
+ workInProgressRootUpdatedLanes = mergeLanes(
578
+ workInProgressRootUpdatedLanes,
579
+ lane,
580
+ );
581
+ }
582
if (workInProgressRootExitStatus === RootSuspendedWithDelay) {
583
// The root already suspended with a delay, which means this render
584
// definitely won't finish. Since we have a new update, let's mark it as
585
// suspended now, right before marking the incoming update. This has the
586
// effect of interrupting the current render and switching to the update.
560
- // TODO: This happens to work when receiving an update during the render
561
- // phase, because of the trick inside computeExpirationForFiber to
562
- // subtract 1 from `renderExpirationTime` to move it into a
563
- // separate bucket. But we should probably model it with an exception,
564
- // using the same mechanism we use to force hydration of a subtree.
565
- // TODO: This does not account for low pri updates that were already
566
- // scheduled before the root started rendering. Need to track the next
567
- // pending expiration time (perhaps by backtracking the return path) and
568
- // then trigger a restart in the `renderDidSuspendDelayIfPossible` path.
569
- markRootSuspendedAtTime(root, renderExpirationTime);
587
+ // TODO: Make sure this doesn't override pings that happen while we've
588
+ // already started rendering.
589
+ markRootSuspended(root, workInProgressRootRenderLanes);
590
}
591
}
572
- // Mark that the root has a pending update.
573
- markRootUpdatedAtTime(root, expirationTime);
592
}
593
594
return root;
595
}
596
579
-function getNextRootExpirationTimeToWorkOn(root: FiberRoot): ExpirationTime {
580
- // Determines the next expiration time that the root should render, taking
581
- // into account levels that may be suspended, or levels that may have
582
- // received a ping.
583
-
584
- const lastExpiredTime = root.lastExpiredTime;
585
- if (lastExpiredTime !== NoWork) {
586
- return lastExpiredTime;
587
- }
588
-
589
- // "Pending" refers to any update that hasn't committed yet, including if it
590
- // suspended. The "suspended" range is therefore a subset.
591
- const firstPendingTime = root.firstPendingTime;
592
- if (!isRootSuspendedAtTime(root, firstPendingTime)) {
593
- // The highest priority pending time is not suspended. Let's work on that.
594
- return firstPendingTime;
595
- }
596
-
597
- // If the first pending time is suspended, check if there's a lower priority
598
- // pending level that we know about. Or check if we received a ping. Work
599
- // on whichever is higher priority.
600
- const lastPingedTime = root.lastPingedTime;
601
- const nextKnownPendingLevel = root.nextKnownPendingLevel;
602
- const nextLevel =
603
- lastPingedTime > nextKnownPendingLevel
604
- ? lastPingedTime
605
- : nextKnownPendingLevel;
606
- if (nextLevel <= Idle && firstPendingTime !== nextLevel) {
607
- // Don't work on Idle/Never priority unless everything else is committed.
608
- return NoWork;
609
- }
610
- return nextLevel;
611
-}
612
-
597
// Use this function to schedule a task for a root. There's only one task per
598
// root; if a task was already scheduled, we'll check to make sure the
599
// expiration time of the existing task is the same as the expiration time of
600
// the next level that the root has work on. This function is called on every
601
// update, and right before exiting a task.
618
-function ensureRootIsScheduled(root: FiberRoot) {
619
- const lastExpiredTime = root.lastExpiredTime;
620
- if (lastExpiredTime !== NoWork) {
621
- // Special case: Expired work should flush synchronously.
622
- root.callbackExpirationTime = Sync;
623
- root.callbackPriority_old = ImmediatePriority;
624
- root.callbackNode = scheduleSyncCallback(
625
- performSyncWorkOnRoot.bind(null, root),
626
- );
627
- return;
628
- }
629
-
630
- const expirationTime = getNextRootExpirationTimeToWorkOn(root);
602
+function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
603
const existingCallbackNode = root.callbackNode;
632
- if (expirationTime === NoWork) {
633
- // There's nothing to work on.
604
+
605
+ // Check if any lanes are being starved by other work. If so, mark them as
606
+ // expired so we know to work on those next.
607
+ markStarvedLanesAsExpired(root, currentTime);
608
+
609
+ // Determine the next lanes to work on, and their priority.
610
+ const newCallbackId = getNextLanes(
611
+ root,
612
+ root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
613
+ );
614
+ // This returns the priority level computed during the `getNextLanes` call.
615
+ const newCallbackPriority = returnNextLanesPriority();
616
+
617
+ if (newCallbackId === NoLanes) {
618
+ // Special case: There's nothing to work on.
619
if (existingCallbackNode !== null) {
620
+ cancelCallback(existingCallbackNode);
621
root.callbackNode = null;
636
- root.callbackExpirationTime = NoWork;
637
- root.callbackPriority_old = NoPriority;
622
+ root.callbackPriority = NoLanePriority;
623
+ root.callbackId = NoLanes;
624
}
625
return;
626
}
627
642
- // TODO: If this is an update, we already read the current time. Pass the
643
- // time as an argument.
644
- const currentTime = requestCurrentTimeForUpdate();
645
- const priorityLevel = inferPriorityFromExpirationTime(
646
- currentTime,
647
- expirationTime,
648
- );
649
-
650
- // If there's an existing render task, confirm it has the correct priority and
651
- // expiration time. Otherwise, we'll cancel it and schedule a new one.
652
- if (existingCallbackNode !== null) {
653
- const existingCallbackPriority = root.callbackPriority_old;
654
- const existingCallbackExpirationTime = root.callbackExpirationTime;
655
- if (
656
- // Callback must have the exact same expiration time.
657
- existingCallbackExpirationTime === expirationTime &&
658
- // Callback must have greater or equal priority.
659
- existingCallbackPriority >= priorityLevel
660
- ) {
661
- // Existing callback is sufficient.
662
- return;
628
+ // Check if there's an existing task. We may be able to reuse it.
629
+ const existingCallbackId = root.callbackId;
630
+ const existingCallbackPriority = root.callbackPriority;
631
+ if (existingCallbackId !== NoLanes) {
632
+ if (newCallbackId === existingCallbackId) {
633
+ // This task is already scheduled. Let's check its priority.
634
+ if (existingCallbackPriority === newCallbackPriority) {
635
+ // The priority hasn't changed. Exit.
636
+ return;
637
+ }
638
+ // The task ID is the same but the priority changed. Cancel the existing
639
+ // callback. We'll schedule a new one below.
640
}
664
- // Need to schedule a new task.
665
- // TODO: Instead of scheduling a new task, we should be able to change the
666
- // priority of the existing one.
641
cancelCallback(existingCallbackNode);
642
}
643
670
- root.callbackExpirationTime = expirationTime;
671
- root.callbackPriority_old = priorityLevel;
672
-
673
- let callbackNode;
674
- if (expirationTime === Sync) {
675
- // Sync React callbacks are scheduled on a special internal queue
676
- callbackNode = scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
677
- } else if (disableSchedulerTimeoutBasedOnReactExpirationTime) {
678
- callbackNode = scheduleCallback(
679
- priorityLevel,
680
- performConcurrentWorkOnRoot.bind(null, root),
644
+ // Schedule a new callback.
645
+ let newCallbackNode;
646
+ if (newCallbackPriority === SyncLanePriority) {
647
+ // Special case: Sync React callbacks are scheduled on a special
648
+ // internal queue
649
+ newCallbackNode = scheduleSyncCallback(
650
+ performSyncWorkOnRoot.bind(null, root),
651
);
652
} else {
683
- callbackNode = scheduleCallback(
684
- priorityLevel,
653
+ const schedulerPriorityLevel = lanePriorityToSchedulerPriority(
654
+ newCallbackPriority,
655
+ );
656
+ newCallbackNode = scheduleCallback(
657
+ schedulerPriorityLevel,
658
performConcurrentWorkOnRoot.bind(null, root),
686
- // Compute a task timeout based on the expiration time. This also affects
687
- // ordering because tasks are processed in timeout order.
688
- {timeout: expirationTimeToMs(expirationTime) - now()},
659
);
660
}
661
692
- root.callbackNode = callbackNode;
662
+ root.callbackId = newCallbackId;
663
+ root.callbackPriority = newCallbackPriority;
664
+ root.callbackNode = newCallbackNode;
665
}
666
667
// This is the entry point for every concurrent task, i.e. anything that
669
function performConcurrentWorkOnRoot(root, didTimeout) {
670
// Since we know we're in a React event, we can clear the current
671
// event time. The next update will compute a new event time.
700
- currentEventTime = NoWork;
701
-
702
- // Check if the render expired.
703
- if (didTimeout) {
704
- // The render task took too long to complete. Mark the current time as
705
- // expired to synchronously render all expired work in a single batch.
706
- const currentTime = requestCurrentTimeForUpdate();
707
- markRootExpiredAtTime(root, currentTime);
708
- // This will schedule a synchronous callback.
709
- ensureRootIsScheduled(root);
710
- return null;
711
- }
672
+ currentEventTime = NoTimestamp;
673
+ currentEventWipLanes = NoLanes;
674
+ currentEventPendingLanes = NoLanes;
675
713
- // Determine the next expiration time to work on, using the fields stored
714
- // on the root.
715
- let expirationTime = getNextRootExpirationTimeToWorkOn(root);
716
- if (expirationTime === NoWork) {
717
- return null;
718
- }
719
- const originalCallbackNode = root.callbackNode;
676
invariant(
677
(executionContext & (RenderContext | CommitContext)) === NoContext,
678
'Should not already be working.',
679
);
680
681
+ // Flush any pending passive effects before deciding which lanes to work on,
682
+ // in case they schedule additional work.
683
flushPassiveEffects();
684
727
- let exitStatus = renderRootConcurrent(root, expirationTime);
685
+ // Determine the next expiration time to work on, using the fields stored
686
+ // on the root.
687
+ let lanes = getNextLanes(
688
+ root,
689
+ root === workInProgressRoot ? workInProgressRootRenderLanes : NoLanes,
690
+ );
691
+ if (lanes === NoLanes) {
692
+ return null;
693
+ }
694
+
695
+ // TODO: We only check `didTimeout` defensively, to account for a Scheduler
696
+ // bug where `shouldYield` sometimes returns `true` even if `didTimeout` is
697
+ // true, which leads to an infinite loop. Once the bug in Scheduler is
698
+ // fixed, we can remove this, since we track expiration ourselves.
699
+ if (didTimeout) {
700
+ // Something expired. Flush synchronously until there's no expired
701
+ // work left.
702
+ markRootExpired(root, lanes);
703
+ // This will schedule a synchronous callback.
704
+ ensureRootIsScheduled(root, now());
705
+ return null;
706
+ }
707
+
708
+ const originalCallbackNode = root.callbackNode;
709
+
710
+ let exitStatus = renderRootConcurrent(root, lanes);
711
729
- if (exitStatus !== RootIncomplete) {
712
+ if (
713
+ includesSomeLane(
714
+ workInProgressRootIncludedLanes,
715
+ workInProgressRootUpdatedLanes,
716
+ )
717
+ ) {
718
+ // The render included lanes that were updated during the render phase.
719
+ // For example, when unhiding a hidden tree, we include all the lanes
720
+ // that were previously skipped when the tree was hidden. That set of
721
+ // lanes is a superset of the lanes we started rendering with.
722
+ //
723
+ // So we'll throw out the current work and restart.
724
+ prepareFreshStack(root, NoLanes);
725
+ } else if (exitStatus !== RootIncomplete) {
726
if (exitStatus === RootErrored) {
727
executionContext |= RetryAfterError;
728
733
clearContainer(root.containerInfo);
734
}
735
740
- // If something threw an error, try rendering one more time. We'll
741
- // render synchronously to block concurrent data mutations, and we'll
742
- // render at Idle (or lower) so that all pending updates are included.
743
- // If it still fails after the second attempt, we'll give up and commit
744
- // the resulting tree.
745
- expirationTime = expirationTime > Idle ? Idle : expirationTime;
746
- exitStatus = renderRootSync(root, expirationTime);
736
+ // If something threw an error, try rendering one more time. We'll render
737
+ // synchronously to block concurrent data mutations, and we'll includes
738
+ // all pending updates are included. If it still fails after the second
739
+ // attempt, we'll give up and commit the resulting tree.
740
+ lanes = getLanesToRetrySynchronouslyOnError(root);
741
+ if (lanes !== NoLanes) {
742
+ exitStatus = renderRootSync(root, lanes);
743
+ }
744
}
745
746
if (exitStatus === RootFatalErrored) {
747
const fatalError = workInProgressRootFatalError;
751
- prepareFreshStack(root, expirationTime);
752
- markRootSuspendedAtTime(root, expirationTime);
753
- ensureRootIsScheduled(root);
748
+ prepareFreshStack(root, NoLanes);
749
+ markRootSuspended(root, lanes);
750
+ ensureRootIsScheduled(root, now());
751
throw fatalError;
752
}
753
755
// or, if something suspended, wait to commit it after a timeout.
756
const finishedWork: Fiber = (root.current.alternate: any);
757
root.finishedWork = finishedWork;
761
- root.finishedExpirationTime = expirationTime;
762
- root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
763
- finishConcurrentRender(root, finishedWork, exitStatus, expirationTime);
758
+ root.finishedLanes = lanes;
759
+ finishConcurrentRender(root, finishedWork, exitStatus, lanes);
760
}
761
766
- ensureRootIsScheduled(root);
762
+ ensureRootIsScheduled(root, now());
763
if (root.callbackNode === originalCallbackNode) {
764
// The task node scheduled for this root is the same one that's
765
// currently executed. Need to return a continuation.
768
return null;
769
}
770
775
-function finishConcurrentRender(
776
- root,
777
- finishedWork,
778
- exitStatus,
779
- expirationTime,
780
-) {
771
+function finishConcurrentRender(root, finishedWork, exitStatus, lanes) {
772
switch (exitStatus) {
773
case RootIncomplete:
774
case RootFatalErrored: {
784
break;
785
}
786
case RootSuspended: {
796
- markRootSuspendedAtTime(root, expirationTime);
797
- const lastSuspendedTime = root.lastSuspendedTime;
787
+ markRootSuspended(root, lanes);
788
789
// We have an acceptable loading state. We need to figure out if we
790
// should immediately commit it or wait a bit.
793
// have a new loading state ready. We want to ensure that we commit
794
// that as soon as possible.
795
const hasNotProcessedNewUpdates =
806
- workInProgressRootLatestProcessedExpirationTime === Sync;
796
+ workInProgressRootLatestProcessedEventTime === NoTimestamp;
797
if (
798
hasNotProcessedNewUpdates &&
799
// do not delay if we're inside an act() scope
809
globalMostRecentFallbackTime + FALLBACK_THROTTLE_MS - now();
810
// Don't bother with a very short suspense time.
811
if (msUntilTimeout > 10) {
822
- if (workInProgressRootHasPendingPing) {
823
- const lastPingedTime = root.lastPingedTime;
824
- if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
825
- // This render was pinged but we didn't get to restart
826
- // earlier so try restarting now instead.
827
- root.lastPingedTime = expirationTime;
828
- prepareFreshStack(root, expirationTime);
829
- break;
830
- }
831
- }
832
-
833
- const nextTime = getNextRootExpirationTimeToWorkOn(root);
834
- if (nextTime !== NoWork && nextTime !== expirationTime) {
812
+ const nextLanes = getNextLanes(root, NoLanes);
813
+ if (nextLanes !== NoLanes) {
814
// There's additional work on this root.
815
break;
816
}
838
- if (
839
- lastSuspendedTime !== NoWork &&
840
- lastSuspendedTime !== expirationTime
841
- ) {
817
+ const suspendedLanes = root.suspendedLanes;
818
+ if (!isSubsetOfLanes(suspendedLanes, lanes)) {
819
// We should prefer to render the fallback of at the last
820
// suspended level. Ping the last suspended level to try
821
// rendering it again.
845
- root.lastPingedTime = lastSuspendedTime;
822
+ // FIXME: What if the suspended lanes are Idle? Should not restart.
823
+ const eventTime = requestEventTime();
824
+ markRootPinged(root, suspendedLanes, eventTime);
825
break;
826
}
827
840
break;
841
}
842
case RootSuspendedWithDelay: {
864
- markRootSuspendedAtTime(root, expirationTime);
865
- const lastSuspendedTime = root.lastSuspendedTime;
843
+ markRootSuspended(root, lanes);
844
845
if (
846
// do not delay if we're inside an act() scope
848
) {
849
// We're suspended in a state that should be avoided. We'll try to
850
// avoid committing it for as long as the timeouts let us.
873
- if (workInProgressRootHasPendingPing) {
874
- const lastPingedTime = root.lastPingedTime;
875
- if (lastPingedTime === NoWork || lastPingedTime >= expirationTime) {
876
- // This render was pinged but we didn't get to restart earlier
877
- // so try restarting now instead.
878
- root.lastPingedTime = expirationTime;
879
- prepareFreshStack(root, expirationTime);
880
- break;
881
- }
882
- }
883
-
884
- const nextTime = getNextRootExpirationTimeToWorkOn(root);
885
- if (nextTime !== NoWork && nextTime !== expirationTime) {
851
+ const nextLanes = getNextLanes(root, NoLanes);
852
+ if (nextLanes !== NoLanes) {
853
// There's additional work on this root.
854
break;
855
}
889
- if (
890
- lastSuspendedTime !== NoWork &&
891
- lastSuspendedTime !== expirationTime
892
- ) {
856
+ const suspendedLanes = root.suspendedLanes;
857
+ if (!isSubsetOfLanes(suspendedLanes, lanes)) {
858
// We should prefer to render the fallback of at the last
859
// suspended level. Ping the last suspended level to try
860
// rendering it again.
896
- root.lastPingedTime = lastSuspendedTime;
861
+ // FIXME: What if the suspended lanes are Idle? Should not restart.
862
+ const eventTime = requestEventTime();
863
+ markRootPinged(root, suspendedLanes, eventTime);
864
break;
865
}
866
867
let msUntilTimeout;
901
- if (workInProgressRootLatestSuspenseTimeout !== Sync) {
868
+ if (workInProgressRootLatestSuspenseTimeout !== NoTimestamp) {
869
// We have processed a suspense config whose expiration time we
870
// can use as the timeout.
904
- msUntilTimeout =
905
- expirationTimeToMs(workInProgressRootLatestSuspenseTimeout) - now();
906
- } else if (workInProgressRootLatestProcessedExpirationTime === Sync) {
871
+ msUntilTimeout = workInProgressRootLatestSuspenseTimeout - now();
872
+ } else if (workInProgressRootLatestProcessedEventTime === NoTimestamp) {
873
// This should never normally happen because only new updates
874
// cause delayed states, so we should have processed something.
875
// However, this could also happen in an offscreen tree.
876
msUntilTimeout = 0;
877
} else {
912
- // If we don't have a suspense config, we're going to use a
913
- // heuristic to determine how long we can suspend.
914
- const eventTimeMs: number = inferTimeFromExpirationTime(
915
- workInProgressRootLatestProcessedExpirationTime,
916
- );
917
- const currentTimeMs = now();
918
- const timeUntilExpirationMs =
919
- expirationTimeToMs(expirationTime) - currentTimeMs;
920
- let timeElapsed = currentTimeMs - eventTimeMs;
921
- if (timeElapsed < 0) {
922
- // We get this wrong some time since we estimate the time.
923
- timeElapsed = 0;
924
- }
925
-
926
- msUntilTimeout = jnd(timeElapsed) - timeElapsed;
927
-
928
- // Clamp the timeout to the expiration time. TODO: Once the
929
- // event time is exact instead of inferred from expiration time
930
- // we don't need this.
931
- if (timeUntilExpirationMs < msUntilTimeout) {
932
- msUntilTimeout = timeUntilExpirationMs;
933
- }
878
+ // If we didn't process a suspense config, compute a JND based on
879
+ // the amount of time elapsed since the most recent event time.
880
+ const eventTimeMs = workInProgressRootLatestProcessedEventTime;
881
+ const timeElapsedMs = now() - eventTimeMs;
882
+ msUntilTimeout = jnd(timeElapsedMs) - timeElapsedMs;
883
}
884
885
// Don't bother with a very short suspense time.
903
if (
904
// do not delay if we're inside an act() scope
905
!shouldForceFlushFallbacksInDEV() &&
957
- workInProgressRootLatestProcessedExpirationTime !== Sync &&
906
+ workInProgressRootLatestProcessedEventTime !== NoTimestamp &&
907
workInProgressRootCanSuspendUsingConfig !== null
908
) {
909
// If we have exceeded the minimum loading delay, which probably
911
// a bit longer to ensure that the spinner is shown for
912
// enough time.
913
const msUntilTimeout = computeMsUntilSuspenseLoadingDelay(
965
- workInProgressRootLatestProcessedExpirationTime,
966
- expirationTime,
914
+ workInProgressRootLatestProcessedEventTime,
915
workInProgressRootCanSuspendUsingConfig,
916
);
917
if (msUntilTimeout > 10) {
970
- markRootSuspendedAtTime(root, expirationTime);
918
+ markRootSuspended(root, lanes);
919
root.timeoutHandle = scheduleTimeout(
920
commitRoot.bind(null, root),
921
msUntilTimeout,
932
}
933
}
934
935
+function markRootSuspended(root, suspendedLanes) {
936
+ // When suspending, we should always exclude lanes that were pinged or (more
937
+ // rarely, since we try to avoid it) updated during the render phase.
938
+ // TODO: Lol maybe there's a better way to factor this besides this
939
+ // obnoxiously named function :)
940
+ suspendedLanes = removeLanes(suspendedLanes, workInProgressRootPingedLanes);
941
+ suspendedLanes = removeLanes(suspendedLanes, workInProgressRootUpdatedLanes);
942
+ markRootSuspended_dontCallThisOneDirectly(root, suspendedLanes);
943
+}
944
+
945
// This is the entry point for synchronous tasks that don't go
946
// through Scheduler
947
function performSyncWorkOnRoot(root) {
952
953
flushPassiveEffects();
954
997
- const lastExpiredTime = root.lastExpiredTime;
998
-
999
- let expirationTime;
1000
- if (lastExpiredTime !== NoWork) {
1001
- // There's expired work on this root. Check if we have a partial tree
1002
- // that we can reuse.
955
+ let lanes;
956
+ let exitStatus;
957
+ if (
958
+ root === workInProgressRoot &&
959
+ includesSomeLane(root.expiredLanes, workInProgressRootRenderLanes)
960
+ ) {
961
+ // There's a partial tree, and at least one of its lanes has expired. Finish
962
+ // rendering it before rendering the rest of the expired work.
963
+ lanes = workInProgressRootRenderLanes;
964
+ exitStatus = renderRootSync(root, lanes);
965
if (
1004
- root === workInProgressRoot &&
1005
- renderExpirationTime >= lastExpiredTime
966
+ includesSomeLane(
967
+ workInProgressRootIncludedLanes,
968
+ workInProgressRootUpdatedLanes,
969
+ )
970
) {
1007
- // There's a partial tree with equal or greater than priority than the
1008
- // expired level. Finish rendering it before rendering the rest of the
1009
- // expired work.
1010
- expirationTime = renderExpirationTime;
1011
- } else {
1012
- // Start a fresh tree.
1013
- expirationTime = lastExpiredTime;
971
+ // The render included lanes that were updated during the render phase.
972
+ // For example, when unhiding a hidden tree, we include all the lanes
973
+ // that were previously skipped when the tree was hidden. That set of
974
+ // lanes is a superset of the lanes we started rendering with.
975
+ //
976
+ // Note that this only happens when part of the tree is rendered
977
+ // concurrently. If the whole tree is rendered synchronously, then there
978
+ // are no interleaved events.
979
+ lanes = getNextLanes(root, lanes);
980
+ exitStatus = renderRootSync(root, lanes);
981
}
982
} else {
1016
- // There's no expired work. This must be a new, synchronous render.
1017
- expirationTime = Sync;
983
+ lanes = getNextLanes(root, NoLanes);
984
+ exitStatus = renderRootSync(root, lanes);
985
}
986
1020
- let exitStatus = renderRootSync(root, expirationTime);
1021
-
987
if (root.tag !== LegacyRoot && exitStatus === RootErrored) {
988
executionContext |= RetryAfterError;
989
994
clearContainer(root.containerInfo);
995
}
996
1032
- // If something threw an error, try rendering one more time. We'll
1033
- // render synchronously to block concurrent data mutations, and we'll
1034
- // render at Idle (or lower) so that all pending updates are included.
1035
- // If it still fails after the second attempt, we'll give up and commit
1036
- // the resulting tree.
1037
- expirationTime = expirationTime > Idle ? Idle : expirationTime;
1038
- exitStatus = renderRootSync(root, expirationTime);
997
+ // If something threw an error, try rendering one more time. We'll render
998
+ // synchronously to block concurrent data mutations, and we'll includes
999
+ // all pending updates are included. If it still fails after the second
1000
+ // attempt, we'll give up and commit the resulting tree.
1001
+ lanes = getLanesToRetrySynchronouslyOnError(root);
1002
+ if (lanes !== NoLanes) {
1003
+ exitStatus = renderRootSync(root, lanes);
1004
+ }
1005
}
1006
1007
if (exitStatus === RootFatalErrored) {
1008
const fatalError = workInProgressRootFatalError;
1043
- prepareFreshStack(root, expirationTime);
1044
- markRootSuspendedAtTime(root, expirationTime);
1045
- ensureRootIsScheduled(root);
1009
+ prepareFreshStack(root, NoLanes);
1010
+ markRootSuspended(root, lanes);
1011
+ ensureRootIsScheduled(root, now());
1012
throw fatalError;
1013
}
1014
1016
// will commit it even if something suspended.
1017
const finishedWork: Fiber = (root.current.alternate: any);
1018
root.finishedWork = finishedWork;
1053
- root.finishedExpirationTime = expirationTime;
1054
- root.nextKnownPendingLevel = getRemainingExpirationTime(finishedWork);
1019
+ root.finishedLanes = lanes;
1020
commitRoot(root);
1021
1022
// Before exiting, make sure there's a callback scheduled for the next
1023
// pending level.
1059
- ensureRootIsScheduled(root);
1024
+ ensureRootIsScheduled(root, now());
1025
1026
return null;
1027
}
1028
1064
-export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
1065
- markRootExpiredAtTime(root, expirationTime);
1066
- ensureRootIsScheduled(root);
1029
+export function flushRoot(root: FiberRoot, lanes: Lanes) {
1030
+ markRootExpired(root, lanes);
1031
+ ensureRootIsScheduled(root, now());
1032
if ((executionContext & (RenderContext | CommitContext)) === NoContext) {
1033
flushSyncCallbackQueue();
1034
}
1068
1069
export function deferredUpdates<A>(fn: () => A): A {
1070
// TODO: Remove in favor of Scheduler.next
1106
- return runWithPriority(NormalPriority, fn);
1071
+ return runWithPriority(NormalSchedulerPriority, fn);
1072
}
1073
1074
function flushPendingDiscreteUpdates() {
1077
// immediately flush them.
1078
const roots = rootsWithPendingDiscreteUpdates;
1079
rootsWithPendingDiscreteUpdates = null;
1115
- roots.forEach((expirationTime, root) => {
1116
- markRootExpiredAtTime(root, expirationTime);
1117
- ensureRootIsScheduled(root);
1080
+ roots.forEach(root => {
1081
+ markDiscreteUpdatesExpired(root);
1082
+ ensureRootIsScheduled(root, now());
1083
});
1084
}
1085
// Now flush the immediate queue.
1125
executionContext |= DiscreteEventContext;
1126
try {
1127
// Should this
1163
- return runWithPriority(UserBlockingPriority, fn.bind(null, a, b, c, d));
1128
+ return runWithPriority(
1129
+ UserBlockingSchedulerPriority,
1130
+ fn.bind(null, a, b, c, d),
1131
+ );
1132
} finally {
1133
executionContext = prevExecutionContext;
1134
if (executionContext === NoContext) {
1168
executionContext |= BatchedContext;
1169
try {
1170
if (fn) {
1203
- return runWithPriority(ImmediatePriority, fn.bind(null, a));
1171
+ return runWithPriority(ImmediateSchedulerPriority, fn.bind(null, a));
1172
} else {
1173
return (undefined: $FlowFixMe);
1174
}
1185
const prevExecutionContext = executionContext;
1186
executionContext |= BatchedContext;
1187
try {
1220
- runWithPriority(ImmediatePriority, fn);
1188
+ runWithPriority(ImmediateSchedulerPriority, fn);
1189
} finally {
1190
executionContext = prevExecutionContext;
1191
if (executionContext === NoContext) {
1195
}
1196
}
1197
1230
-function prepareFreshStack(root, expirationTime) {
1198
+export function pushRenderLanes(fiber: Fiber, lanes: Lanes) {
1199
+ pushToStack(subtreeRenderLanesCursor, subtreeRenderLanes, fiber);
1200
+ subtreeRenderLanes = mergeLanes(subtreeRenderLanes, lanes);
1201
+ workInProgressRootIncludedLanes = mergeLanes(
1202
+ workInProgressRootIncludedLanes,
1203
+ lanes,
1204
+ );
1205
+}
1206
+
1207
+export function popRenderLanes(fiber: Fiber) {
1208
+ subtreeRenderLanes = subtreeRenderLanesCursor.current;
1209
+ popFromStack(subtreeRenderLanesCursor, fiber);
1210
+}
1211
+
1212
+function prepareFreshStack(root: FiberRoot, lanes: Lanes) {
1213
root.finishedWork = null;
1232
- root.finishedExpirationTime = NoWork;
1214
+ root.finishedLanes = NoLanes;
1215
1216
const timeoutHandle = root.timeoutHandle;
1217
if (timeoutHandle !== noTimeout) {
1222
cancelTimeout(timeoutHandle);
1223
}
1224
1243
- // Check if there's a suspended level at lower priority.
1244
- const lastSuspendedTime = root.lastSuspendedTime;
1245
- if (lastSuspendedTime !== NoWork && lastSuspendedTime < expirationTime) {
1246
- const lastPingedTime = root.lastPingedTime;
1247
- // Make sure the suspended level is marked as pinged so that we return back
1248
- // to it later, in case the render we're about to start gets aborted.
1249
- // Generally we only reach this path via a ping, but we shouldn't assume
1250
- // that will always be the case.
1251
- // Note: This is defensive coding to prevent a pending commit from
1252
- // being dropped without being rescheduled. It shouldn't be necessary.
1253
- if (lastPingedTime === NoWork || lastPingedTime > lastSuspendedTime) {
1254
- root.lastPingedTime = lastSuspendedTime;
1255
- }
1256
- }
1257
-
1225
if (workInProgress !== null) {
1226
let interruptedWork = workInProgress.return;
1227
while (interruptedWork !== null) {
1231
}
1232
workInProgressRoot = root;
1233
workInProgress = createWorkInProgress(root.current, null);
1267
- renderExpirationTime = expirationTime;
1234
+ workInProgressRootRenderLanes = subtreeRenderLanes = workInProgressRootIncludedLanes = lanes;
1235
workInProgressRootExitStatus = RootIncomplete;
1236
workInProgressRootFatalError = null;
1270
- workInProgressRootLatestProcessedExpirationTime = Sync;
1271
- workInProgressRootLatestSuspenseTimeout = Sync;
1237
+ workInProgressRootLatestProcessedEventTime = NoTimestamp;
1238
+ workInProgressRootLatestSuspenseTimeout = NoTimestamp;
1239
workInProgressRootCanSuspendUsingConfig = null;
1273
- workInProgressRootNextUnprocessedUpdateTime = NoWork;
1274
- workInProgressRootHasPendingPing = false;
1240
+ workInProgressRootSkippedLanes = NoLanes;
1241
+ workInProgressRootUpdatedLanes = NoLanes;
1242
+ workInProgressRootPingedLanes = NoLanes;
1243
1244
if (enableSchedulerTracing) {
1245
spawnedWorkDuringRender = null;
1291
erroredWork.return,
1292
erroredWork,
1293
thrownValue,
1326
- renderExpirationTime,
1294
+ workInProgressRootRenderLanes,
1295
);
1296
completeUnitOfWork(erroredWork);
1297
} catch (yetAnotherThrownValue) {
1349
}
1350
1351
export function markRenderEventTimeAndConfig(
1384
- expirationTime: ExpirationTime,
1352
+ eventTime: number,
1353
suspenseConfig: null | SuspenseConfig,
1354
): void {
1387
- if (
1388
- expirationTime < workInProgressRootLatestProcessedExpirationTime &&
1389
- expirationTime > Idle
1390
- ) {
1391
- workInProgressRootLatestProcessedExpirationTime = expirationTime;
1355
+ // Track the most recent event time of all updates processed in this batch.
1356
+ if (workInProgressRootLatestProcessedEventTime < eventTime) {
1357
+ workInProgressRootLatestProcessedEventTime = eventTime;
1358
}
1359
+
1360
+ // Track the largest/latest timeout deadline in this batch.
1361
+ // TODO: If there are two transitions in the same batch, shouldn't we
1362
+ // choose the smaller one? Maybe this is because when an intermediate
1363
+ // transition is superseded, we should ignore its suspense config, but
1364
+ // we don't currently.
1365
if (suspenseConfig !== null) {
1394
- if (
1395
- expirationTime < workInProgressRootLatestSuspenseTimeout &&
1396
- expirationTime > Idle
1397
- ) {
1398
- workInProgressRootLatestSuspenseTimeout = expirationTime;
1399
- // Most of the time we only have one config and getting wrong is not bad.
1366
+ // If `timeoutMs` is not specified, we default to 5 seconds. We have to
1367
+ // resolve this default here because `suspenseConfig` is owned
1368
+ // by userspace.
1369
+ // TODO: Store this on the root instead (transition -> timeoutMs)
1370
+ // TODO: Should this default to a JND instead?
1371
+ const timeoutMs = suspenseConfig.timeoutMs | 0 || DEFAULT_TIMEOUT_MS;
1372
+ const timeoutTime = eventTime + timeoutMs;
1373
+ if (timeoutTime > workInProgressRootLatestSuspenseTimeout) {
1374
+ workInProgressRootLatestSuspenseTimeout = timeoutTime;
1375
workInProgressRootCanSuspendUsingConfig = suspenseConfig;
1376
}
1377
}
1378
}
1379
1405
-export function markUnprocessedUpdateTime(
1406
- expirationTime: ExpirationTime,
1407
-): void {
1408
- if (expirationTime > workInProgressRootNextUnprocessedUpdateTime) {
1409
- workInProgressRootNextUnprocessedUpdateTime = expirationTime;
1410
- }
1380
+export function markSkippedUpdateLanes(lane: Lane | Lanes): void {
1381
+ workInProgressRootSkippedLanes = mergeLanes(
1382
+ lane,
1383
+ workInProgressRootSkippedLanes,
1384
+ );
1385
}
1386
1387
export function renderDidSuspend(): void {
1398
workInProgressRootExitStatus = RootSuspendedWithDelay;
1399
}
1400
1427
- // Check if there's a lower priority update somewhere else in the tree.
1401
+ // Check if there are updates that we skipped tree that might have unblocked
1402
+ // this render.
1403
if (
1429
- workInProgressRootNextUnprocessedUpdateTime !== NoWork &&
1430
- workInProgressRoot !== null
1404
+ workInProgressRoot !== null &&
1405
+ (hasUpdatePriority(workInProgressRootSkippedLanes) ||
1406
+ hasUpdatePriority(workInProgressRootUpdatedLanes))
1407
) {
1432
- // Mark the current render as suspended, and then mark that there's a
1433
- // pending update.
1434
- // TODO: This should immediately interrupt the current render, instead
1435
- // of waiting until the next time we yield.
1436
- markRootSuspendedAtTime(workInProgressRoot, renderExpirationTime);
1437
- markRootUpdatedAtTime(
1438
- workInProgressRoot,
1439
- workInProgressRootNextUnprocessedUpdateTime,
1440
- );
1408
+ // Mark the current render as suspended so that we switch to working on
1409
+ // the updates that were skipped. Usually we only suspend at the end of
1410
+ // the render phase.
1411
+ // TODO: We should probably always mark the root as suspended immediately
1412
+ // (inside this function), since by suspending at the end of the render
1413
+ // phase introduces a potential mistake where we suspend lanes that were
1414
+ // pinged or updated while we were rendering.
1415
+ markRootSuspended(workInProgressRoot, workInProgressRootRenderLanes);
1416
}
1417
}
1418
1430
return workInProgressRootExitStatus === RootIncomplete;
1431
}
1432
1458
-function inferTimeFromExpirationTime(expirationTime: ExpirationTime): number {
1459
- // We don't know exactly when the update was scheduled, but we can infer an
1460
- // approximate start time from the expiration time.
1461
- const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
1462
- return earliestExpirationTimeMs - LOW_PRIORITY_EXPIRATION;
1463
-}
1464
-
1465
-function inferTimeFromExpirationTimeWithSuspenseConfig(
1466
- expirationTime: ExpirationTime,
1467
- suspenseConfig: SuspenseConfig,
1468
-): number {
1469
- // We don't know exactly when the update was scheduled, but we can infer an
1470
- // approximate start time from the expiration time by subtracting the timeout
1471
- // that was added to the event time.
1472
- const earliestExpirationTimeMs = expirationTimeToMs(expirationTime);
1473
- return (
1474
- earliestExpirationTimeMs -
1475
- (suspenseConfig.timeoutMs | 0 || LOW_PRIORITY_EXPIRATION)
1476
- );
1477
-}
1478
-
1479
-function renderRootSync(root, expirationTime) {
1433
+function renderRootSync(root: FiberRoot, lanes: Lanes) {
1434
const prevExecutionContext = executionContext;
1435
executionContext |= RenderContext;
1436
const prevDispatcher = pushDispatcher(root);
1437
1484
- // If the root or expiration time have changed, throw out the existing stack
1438
+ // If the root or lanes have changed, throw out the existing stack
1439
// and prepare a fresh one. Otherwise we'll continue where we left off.
1486
- if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1487
- prepareFreshStack(root, expirationTime);
1488
- startWorkOnPendingInteractions(root, expirationTime);
1440
+ if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1441
+ prepareFreshStack(root, lanes);
1442
+ startWorkOnPendingInteractions(root, lanes);
1443
}
1444
1445
const prevInteractions = pushInteractions(root);
1446
1493
- if (__DEV__) {
1494
- if (enableDebugTracing) {
1495
- const priorityLevel = getCurrentPriorityLevel();
1496
- const label = priorityLevelToLabel(priorityLevel);
1497
- logRenderStarted(label);
1498
- }
1499
- }
1500
-
1447
do {
1448
try {
1449
workLoopSync();
1469
);
1470
}
1471
1526
- if (__DEV__) {
1527
- if (enableDebugTracing) {
1528
- logRenderStopped();
1529
- }
1530
- }
1531
-
1472
// Set this to null to indicate there's no in-progress render.
1473
workInProgressRoot = null;
1474
+ workInProgressRootRenderLanes = NoLanes;
1475
1476
return workInProgressRootExitStatus;
1477
}
1485
}
1486
}
1487
1547
-function renderRootConcurrent(root, expirationTime) {
1488
+function renderRootConcurrent(root: FiberRoot, lanes: Lanes) {
1489
const prevExecutionContext = executionContext;
1490
executionContext |= RenderContext;
1491
const prevDispatcher = pushDispatcher(root);
1492
1552
- // If the root or expiration time have changed, throw out the existing stack
1493
+ // If the root or lanes have changed, throw out the existing stack
1494
// and prepare a fresh one. Otherwise we'll continue where we left off.
1554
- if (root !== workInProgressRoot || expirationTime !== renderExpirationTime) {
1555
- prepareFreshStack(root, expirationTime);
1556
- startWorkOnPendingInteractions(root, expirationTime);
1495
+ if (workInProgressRoot !== root || workInProgressRootRenderLanes !== lanes) {
1496
+ prepareFreshStack(root, lanes);
1497
+ startWorkOnPendingInteractions(root, lanes);
1498
}
1499
1500
const prevInteractions = pushInteractions(root);
1501
1561
- if (__DEV__) {
1562
- if (enableDebugTracing) {
1563
- const priorityLevel = getCurrentPriorityLevel();
1564
- const label = priorityLevelToLabel(priorityLevel);
1565
- logRenderStarted(label);
1566
- }
1567
- }
1568
-
1502
do {
1503
try {
1504
workLoopConcurrent();
1515
popDispatcher(prevDispatcher);
1516
executionContext = prevExecutionContext;
1517
1585
- if (__DEV__) {
1586
- if (enableDebugTracing) {
1587
- logRenderStopped();
1588
- }
1589
- }
1590
-
1518
// Check if the tree has completed.
1519
if (workInProgress !== null) {
1520
// Still work remaining.
1523
// Completed the tree.
1524
// Set this to null to indicate there's no in-progress render.
1525
workInProgressRoot = null;
1526
+ workInProgressRootRenderLanes = NoLanes;
1527
1528
// Return the final exit status.
1529
return workInProgressRootExitStatus;
1548
let next;
1549
if (enableProfilerTimer && (unitOfWork.mode & ProfileMode) !== NoMode) {
1550
startProfilerTimer(unitOfWork);
1623
- next = beginWork(current, unitOfWork, renderExpirationTime);
1551
+ next = beginWork(current, unitOfWork, subtreeRenderLanes);
1552
stopProfilerTimerIfRunningAndRecordDelta(unitOfWork, true);
1553
} else {
1626
- next = beginWork(current, unitOfWork, renderExpirationTime);
1554
+ next = beginWork(current, unitOfWork, subtreeRenderLanes);
1555
}
1556
1557
resetCurrentDebugFiberInDEV();
1585
!enableProfilerTimer ||
1586
(completedWork.mode & ProfileMode) === NoMode
1587
) {
1660
- next = completeWork(current, completedWork, renderExpirationTime);
1588
+ next = completeWork(current, completedWork, subtreeRenderLanes);
1589
} else {
1590
startProfilerTimer(completedWork);
1663
- next = completeWork(current, completedWork, renderExpirationTime);
1591
+ next = completeWork(current, completedWork, subtreeRenderLanes);
1592
// Update render duration assuming we didn't error.
1593
stopProfilerTimerIfRunningAndRecordDelta(completedWork, false);
1594
}
1600
return;
1601
}
1602
1675
- resetChildExpirationTime(completedWork);
1603
+ resetChildLanes(completedWork);
1604
1605
if (
1606
returnFiber !== null &&
1644
// This fiber did not complete because something threw. Pop values off
1645
// the stack without entering the complete phase. If this is a boundary,
1646
// capture values if possible.
1719
- const next = unwindWork(completedWork, renderExpirationTime);
1647
+ const next = unwindWork(completedWork, subtreeRenderLanes);
1648
1649
// Because this fiber did not complete, don't reset its expiration time.
1650
1700
}
1701
}
1702
1775
-function getRemainingExpirationTime(fiber: Fiber) {
1776
- const updateExpirationTime = fiber.expirationTime;
1777
- const childExpirationTime = fiber.childExpirationTime;
1778
- return updateExpirationTime > childExpirationTime
1779
- ? updateExpirationTime
1780
- : childExpirationTime;
1781
-}
1782
-
1783
-function resetChildExpirationTime(completedWork: Fiber) {
1703
+function resetChildLanes(completedWork: Fiber) {
1704
if (
1785
- renderExpirationTime !== Never &&
1786
- completedWork.childExpirationTime === Never
1705
+ // TODO: Move this check out of the hot path by moving `resetChildLanes`
1706
+ // to switch statement in `completeWork`.
1707
+ (completedWork.tag === LegacyHiddenComponent ||
1708
+ completedWork.tag === OffscreenComponent) &&
1709
+ completedWork.memoizedState !== null &&
1710
+ !includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) &&
1711
+ (completedWork.mode & ConcurrentMode) !== NoLanes
1712
) {
1713
// The children of this component are hidden. Don't bubble their
1714
// expiration times.
1715
return;
1716
}
1717
1793
- let newChildExpirationTime = NoWork;
1718
+ let newChildLanes = NoLanes;
1719
1720
// Bubble up the earliest expiration time.
1721
if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
1737
1738
let child = completedWork.child;
1739
while (child !== null) {
1815
- const childUpdateExpirationTime = child.expirationTime;
1816
- const childChildExpirationTime = child.childExpirationTime;
1817
- if (childUpdateExpirationTime > newChildExpirationTime) {
1818
- newChildExpirationTime = childUpdateExpirationTime;
1819
- }
1820
- if (childChildExpirationTime > newChildExpirationTime) {
1821
- newChildExpirationTime = childChildExpirationTime;
1822
- }
1740
+ newChildLanes = mergeLanes(
1741
+ newChildLanes,
1742
+ mergeLanes(child.lanes, child.childLanes),
1743
+ );
1744
if (shouldBubbleActualDurations) {
1745
actualDuration += child.actualDuration;
1746
}
1764
} else {
1765
let child = completedWork.child;
1766
while (child !== null) {
1846
- const childUpdateExpirationTime = child.expirationTime;
1847
- const childChildExpirationTime = child.childExpirationTime;
1848
- if (childUpdateExpirationTime > newChildExpirationTime) {
1849
- newChildExpirationTime = childUpdateExpirationTime;
1850
- }
1851
- if (childChildExpirationTime > newChildExpirationTime) {
1852
- newChildExpirationTime = childChildExpirationTime;
1853
- }
1767
+ newChildLanes = mergeLanes(
1768
+ newChildLanes,
1769
+ mergeLanes(child.lanes, child.childLanes),
1770
+ );
1771
child = child.sibling;
1772
}
1773
}
1774
1858
- completedWork.childExpirationTime = newChildExpirationTime;
1775
+ completedWork.childLanes = newChildLanes;
1776
}
1777
1778
function commitRoot(root) {
1779
const renderPriorityLevel = getCurrentPriorityLevel();
1780
runWithPriority(
1864
- ImmediatePriority,
1781
+ ImmediateSchedulerPriority,
1782
commitRootImpl.bind(null, root, renderPriorityLevel),
1783
);
1784
return null;
1785
}
1786
1787
function commitRootImpl(root, renderPriorityLevel) {
1871
- if (__DEV__) {
1872
- if (enableDebugTracing) {
1873
- const label = priorityLevelToLabel(renderPriorityLevel);
1874
- logCommitStarted(label);
1875
- }
1876
- }
1788
do {
1789
// `flushPassiveEffects` will call `flushSyncUpdateQueue` at the end, which
1790
// means `flushPassiveEffects` will sometimes result in additional
1802
);
1803
1804
const finishedWork = root.finishedWork;
1894
- const expirationTime = root.finishedExpirationTime;
1805
+ const lanes = root.finishedLanes;
1806
if (finishedWork === null) {
1896
- if (__DEV__) {
1897
- if (enableDebugTracing) {
1898
- logCommitStopped();
1899
- }
1900
- }
1807
return null;
1808
}
1809
root.finishedWork = null;
1904
- root.finishedExpirationTime = NoWork;
1810
+ root.finishedLanes = NoLanes;
1811
1812
invariant(
1813
finishedWork !== root.current,
1818
// commitRoot never returns a continuation; it always finishes synchronously.
1819
// So we can clear these now to allow a new callback to be scheduled.
1820
root.callbackNode = null;
1915
- root.callbackExpirationTime = NoWork;
1916
- root.callbackPriority_old = NoPriority;
1821
+ root.callbackId = NoLanes;
1822
1823
// Update the first and last pending times on this root. The new first
1824
// pending time is whatever is left on the root fiber.
1920
- const remainingExpirationTimeBeforeCommit = getRemainingExpirationTime(
1921
- finishedWork,
1922
- );
1923
- markRootFinishedAtTime(
1924
- root,
1925
- expirationTime,
1926
- remainingExpirationTimeBeforeCommit,
1927
- );
1825
+ let remainingLanes = mergeLanes(finishedWork.lanes, finishedWork.childLanes);
1826
+ markRootFinished(root, remainingLanes);
1827
1828
// Clear already finished discrete updates in case that a later call of
1829
// `flushDiscreteUpdates` starts a useless render pass which may cancels
1830
// a scheduled timeout.
1831
if (rootsWithPendingDiscreteUpdates !== null) {
1933
- const lastDiscreteTime = rootsWithPendingDiscreteUpdates.get(root);
1832
if (
1935
- lastDiscreteTime !== undefined &&
1936
- remainingExpirationTimeBeforeCommit < lastDiscreteTime
1833
+ !hasDiscreteLanes(remainingLanes) &&
1834
+ rootsWithPendingDiscreteUpdates.has(root)
1835
) {
1836
rootsWithPendingDiscreteUpdates.delete(root);
1837
}
1841
// We can reset these now that they are finished.
1842
workInProgressRoot = null;
1843
workInProgress = null;
1946
- renderExpirationTime = NoWork;
1844
+ workInProgressRootRenderLanes = NoLanes;
1845
} else {
1846
// This indicates that the last root we worked on is not the same one that
1847
// we're committing now. This most commonly happens when a suspended root
1959
nextEffect = firstEffect;
1960
do {
1961
if (__DEV__) {
2064
- invokeGuardedCallback(
2065
- null,
2066
- commitLayoutEffects,
2067
- null,
2068
- root,
2069
- expirationTime,
2070
- );
1962
+ invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
1963
if (hasCaughtError()) {
1964
invariant(nextEffect !== null, 'Should be working on an effect.');
1965
const error = clearCaughtError();
1968
}
1969
} else {
1970
try {
2079
- commitLayoutEffects(root, expirationTime);
1971
+ commitLayoutEffects(root, lanes);
1972
} catch (error) {
1973
invariant(nextEffect !== null, 'Should be working on an effect.');
1974
captureCommitPhaseError(nextEffect, error);
2005
// schedule a callback until after flushing layout work.
2006
rootDoesHavePassiveEffects = false;
2007
rootWithPendingPassiveEffects = root;
2116
- pendingPassiveEffectsExpirationTime = expirationTime;
2008
+ pendingPassiveEffectsLanes = lanes;
2009
pendingPassiveEffectsRenderPriority = renderPriorityLevel;
2010
} else {
2011
// We are done with the effect chain at this point so let's clear the
2022
}
2023
}
2024
2025
+ // Read this again, since an effect might have updated it
2026
+ remainingLanes = root.pendingLanes;
2027
+
2028
// Check if there's remaining work on this root
2134
- const remainingExpirationTime = root.firstPendingTime;
2135
- if (remainingExpirationTime !== NoWork) {
2029
+ if (remainingLanes !== NoLanes) {
2030
if (enableSchedulerTracing) {
2031
if (spawnedWorkDuringRender !== null) {
2032
const expirationTimes = spawnedWorkDuringRender;
2039
);
2040
}
2041
}
2148
- schedulePendingInteractions(root, remainingExpirationTime);
2042
+ schedulePendingInteractions(root, remainingLanes);
2043
}
2044
} else {
2045
// If there's no remaining work, we can clear the set of already failed
2053
// Otherwise, we'll wait until after the passive effects are flushed.
2054
// Wait to do this until after remaining work has been scheduled,
2055
// so that we don't prematurely signal complete for interactions when there's e.g. hidden work.
2162
- finishPendingInteractions(root, expirationTime);
2056
+ finishPendingInteractions(root, lanes);
2057
}
2058
}
2059
2166
- if (remainingExpirationTime === Sync) {
2060
+ if (remainingLanes === SyncLane) {
2061
// Count the number of times the root synchronously re-renders without
2062
// finishing. If there are too many, it indicates an infinite update loop.
2063
if (root === rootWithNestedUpdates) {
2070
nestedUpdateCount = 0;
2071
}
2072
2179
- onCommitRootDevTools(finishedWork.stateNode, expirationTime);
2073
+ onCommitRootDevTools(finishedWork.stateNode, renderPriorityLevel);
2074
2075
if (__DEV__) {
2076
onCommitRootTestSelector();
2078
2079
// Always call this before exiting `commitRoot`, to ensure that any
2080
// additional work on this root is scheduled.
2187
- ensureRootIsScheduled(root);
2081
+ ensureRootIsScheduled(root, now());
2082
2083
if (hasUncaughtError) {
2084
hasUncaughtError = false;
2088
}
2089
2090
if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
2197
- if (__DEV__) {
2198
- if (enableDebugTracing) {
2199
- logCommitStopped();
2200
- }
2201
- }
2202
-
2091
// This is a legacy edge case. We just committed the initial mount of
2092
// a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
2093
// synchronously, but layout updates should be deferred until the end
2098
// If layout work was scheduled, flush it now.
2099
flushSyncCallbackQueue();
2100
2213
- if (__DEV__) {
2214
- if (enableDebugTracing) {
2215
- logCommitStopped();
2216
- }
2217
- }
2218
-
2101
return null;
2102
}
2103
2137
// the earliest opportunity.
2138
if (!rootDoesHavePassiveEffects) {
2139
rootDoesHavePassiveEffects = true;
2258
- scheduleCallback(NormalPriority, () => {
2140
+ scheduleCallback(NormalSchedulerPriority, () => {
2141
flushPassiveEffects();
2142
return null;
2143
});
2221
}
2222
}
2223
2342
-function commitLayoutEffects(
2343
- root: FiberRoot,
2344
- committedExpirationTime: ExpirationTime,
2345
-) {
2346
- if (__DEV__) {
2347
- if (enableDebugTracing) {
2348
- const priorityLevel = getCurrentPriorityLevel();
2349
- const label = priorityLevelToLabel(priorityLevel);
2350
- logLayoutEffectsStarted(label);
2351
- }
2352
- }
2353
-
2224
+function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
2225
// TODO: Should probably move the bulk of this function to commitWork.
2226
while (nextEffect !== null) {
2227
setCurrentDebugFiberInDEV(nextEffect);
2230
2231
if (effectTag & (Update | Callback)) {
2232
const current = nextEffect.alternate;
2362
- commitLayoutEffectOnFiber(
2363
- root,
2364
- current,
2365
- nextEffect,
2366
- committedExpirationTime,
2367
- );
2233
+ commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
2234
}
2235
2236
if (effectTag & Ref) {
2240
resetCurrentDebugFiberInDEV();
2241
nextEffect = nextEffect.nextEffect;
2242
}
2377
-
2378
- if (__DEV__) {
2379
- if (enableDebugTracing) {
2380
- logLayoutEffectsStopped();
2381
- }
2382
- }
2243
}
2244
2245
export function flushPassiveEffects() {
2386
- if (pendingPassiveEffectsRenderPriority !== NoPriority) {
2246
+ if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
2247
const priorityLevel =
2388
- pendingPassiveEffectsRenderPriority > NormalPriority
2389
- ? NormalPriority
2248
+ pendingPassiveEffectsRenderPriority > NormalSchedulerPriority
2249
+ ? NormalSchedulerPriority
2250
: pendingPassiveEffectsRenderPriority;
2391
- pendingPassiveEffectsRenderPriority = NoPriority;
2251
+ pendingPassiveEffectsRenderPriority = NoSchedulerPriority;
2252
return runWithPriority(priorityLevel, flushPassiveEffectsImpl);
2253
}
2254
}
2258
pendingPassiveProfilerEffects.push(fiber);
2259
if (!rootDoesHavePassiveEffects) {
2260
rootDoesHavePassiveEffects = true;
2401
- scheduleCallback(NormalPriority, () => {
2261
+ scheduleCallback(NormalSchedulerPriority, () => {
2262
flushPassiveEffects();
2263
return null;
2264
});
2273
pendingPassiveHookEffectsMount.push(effect, fiber);
2274
if (!rootDoesHavePassiveEffects) {
2275
rootDoesHavePassiveEffects = true;
2416
- scheduleCallback(NormalPriority, () => {
2276
+ scheduleCallback(NormalSchedulerPriority, () => {
2277
flushPassiveEffects();
2278
return null;
2279
});
2294
}
2295
if (!rootDoesHavePassiveEffects) {
2296
rootDoesHavePassiveEffects = true;
2437
- scheduleCallback(NormalPriority, () => {
2297
+ scheduleCallback(NormalSchedulerPriority, () => {
2298
flushPassiveEffects();
2299
return null;
2300
});
2312
}
2313
2314
const root = rootWithPendingPassiveEffects;
2455
- const expirationTime = pendingPassiveEffectsExpirationTime;
2315
+ const lanes = pendingPassiveEffectsLanes;
2316
rootWithPendingPassiveEffects = null;
2457
- pendingPassiveEffectsExpirationTime = NoWork;
2317
+ pendingPassiveEffectsLanes = NoLanes;
2318
2319
invariant(
2320
(executionContext & (RenderContext | CommitContext)) === NoContext,
2321
'Cannot flush passive effects while already rendering.',
2322
);
2323
2464
- if (__DEV__) {
2465
- if (enableDebugTracing) {
2466
- const priorityLevel = getCurrentPriorityLevel();
2467
- const label = priorityLevelToLabel(priorityLevel);
2468
- logPassiveEffectsStarted(label);
2469
- }
2470
- }
2471
-
2324
if (__DEV__) {
2325
isFlushingPassiveEffects = true;
2326
}
2470
2471
if (enableSchedulerTracing) {
2472
popInteractions(((prevInteractions: any): Set<Interaction>));
2621
- finishPendingInteractions(root, expirationTime);
2473
+ finishPendingInteractions(root, lanes);
2474
}
2475
2476
if (__DEV__) {
2477
isFlushingPassiveEffects = false;
2478
}
2479
2628
- if (__DEV__) {
2629
- if (enableDebugTracing) {
2630
- logPassiveEffectsStopped();
2631
- }
2632
- }
2633
-
2480
executionContext = prevExecutionContext;
2481
2482
flushSyncCallbackQueue();
2518
error: mixed,
2519
) {
2520
const errorInfo = createCapturedValue(error, sourceFiber);
2675
- const update = createRootErrorUpdate(rootFiber, errorInfo, Sync);
2521
+ const update = createRootErrorUpdate(rootFiber, errorInfo, (SyncLane: Lane));
2522
enqueueUpdate(rootFiber, update);
2677
- const root = markUpdateTimeFromFiberToRoot(rootFiber, Sync);
2523
+ const eventTime = requestEventTime();
2524
+ const root = markUpdateLaneFromFiberToRoot(rootFiber, (SyncLane: Lane));
2525
if (root !== null) {
2679
- ensureRootIsScheduled(root);
2680
- schedulePendingInteractions(root, Sync);
2526
+ ensureRootIsScheduled(root, eventTime);
2527
+ schedulePendingInteractions(root, SyncLane);
2528
}
2529
}
2530
2553
const update = createClassErrorUpdate(
2554
fiber,
2555
errorInfo,
2709
- // TODO: This is always sync
2710
- Sync,
2556
+ (SyncLane: Lane),
2557
);
2558
enqueueUpdate(fiber, update);
2713
- const root = markUpdateTimeFromFiberToRoot(fiber, Sync);
2559
+ const eventTime = requestEventTime();
2560
+ const root = markUpdateLaneFromFiberToRoot(fiber, (SyncLane: Lane));
2561
if (root !== null) {
2715
- ensureRootIsScheduled(root);
2716
- schedulePendingInteractions(root, Sync);
2562
+ ensureRootIsScheduled(root, eventTime);
2563
+ schedulePendingInteractions(root, SyncLane);
2564
}
2565
return;
2566
}
2572
export function pingSuspendedRoot(
2573
root: FiberRoot,
2574
wakeable: Wakeable,
2728
- suspendedTime: ExpirationTime,
2575
+ pingedLanes: Lanes,
2576
) {
2577
const pingCache = root.pingCache;
2578
if (pingCache !== null) {
2581
pingCache.delete(wakeable);
2582
}
2583
2737
- if (workInProgressRoot === root && renderExpirationTime === suspendedTime) {
2584
+ const eventTime = requestEventTime();
2585
+ markRootPinged(root, pingedLanes, eventTime);
2586
+
2587
+ if (
2588
+ workInProgressRoot === root &&
2589
+ isSubsetOfLanes(workInProgressRootRenderLanes, pingedLanes)
2590
+ ) {
2591
// Received a ping at the same priority level at which we're currently
2592
// rendering. We might want to restart this render. This should mirror
2593
// the logic of whether or not a root suspends once it completes.
2604
if (
2605
workInProgressRootExitStatus === RootSuspendedWithDelay ||
2606
(workInProgressRootExitStatus === RootSuspended &&
2754
- workInProgressRootLatestProcessedExpirationTime === Sync &&
2607
+ workInProgressRootLatestProcessedEventTime === NoTimestamp &&
2608
now() - globalMostRecentFallbackTime < FALLBACK_THROTTLE_MS)
2609
) {
2757
- // Restart from the root. Don't need to schedule a ping because
2758
- // we're already working on this tree.
2759
- prepareFreshStack(root, renderExpirationTime);
2610
+ // Restart from the root.
2611
+ prepareFreshStack(root, NoLanes);
2612
} else {
2613
// Even though we can't restart right now, we might get an
2614
// opportunity later. So we mark this render as having a ping.
2763
- workInProgressRootHasPendingPing = true;
2615
+ workInProgressRootPingedLanes = mergeLanes(
2616
+ workInProgressRootPingedLanes,
2617
+ pingedLanes,
2618
+ );
2619
}
2765
- return;
2766
- }
2767
-
2768
- if (!isRootSuspendedAtTime(root, suspendedTime)) {
2769
- // The root is no longer suspended at this time.
2770
- return;
2620
}
2621
2773
- const lastPingedTime = root.lastPingedTime;
2774
- if (lastPingedTime !== NoWork && lastPingedTime < suspendedTime) {
2775
- // There's already a lower priority ping scheduled.
2776
- return;
2777
- }
2778
-
2779
- // Mark the time at which this ping was scheduled.
2780
- root.lastPingedTime = suspendedTime;
2781
-
2782
- ensureRootIsScheduled(root);
2783
- schedulePendingInteractions(root, suspendedTime);
2622
+ ensureRootIsScheduled(root, eventTime);
2623
+ schedulePendingInteractions(root, pingedLanes);
2624
}
2625
2786
-function retryTimedOutBoundary(
2787
- boundaryFiber: Fiber,
2788
- retryTime: ExpirationTime,
2789
-) {
2626
+function retryTimedOutBoundary(boundaryFiber: Fiber, retryLane: Lane) {
2627
// The boundary fiber (a Suspense component or SuspenseList component)
2628
// previously was rendered in its fallback state. One of the promises that
2629
// suspended it has resolved, which means at least part of the tree was
2630
// likely unblocked. Try rendering again, at a new expiration time.
2794
- if (retryTime === NoWork) {
2631
+ if (retryLane === NoLane) {
2632
const suspenseConfig = null; // Retries don't carry over the already committed update.
2796
- const currentTime = requestCurrentTimeForUpdate();
2797
- retryTime = computeExpirationForFiber(
2798
- currentTime,
2799
- boundaryFiber,
2800
- suspenseConfig,
2801
- );
2633
+ // TODO: Should retries get their own lane? Maybe it could share with
2634
+ // transitions.
2635
+ retryLane = requestUpdateLane(boundaryFiber, suspenseConfig);
2636
}
2637
// TODO: Special case idle priority?
2804
- const root = markUpdateTimeFromFiberToRoot(boundaryFiber, retryTime);
2638
+ const eventTime = requestEventTime();
2639
+ const root = markUpdateLaneFromFiberToRoot(boundaryFiber, retryLane);
2640
if (root !== null) {
2806
- ensureRootIsScheduled(root);
2807
- schedulePendingInteractions(root, retryTime);
2641
+ ensureRootIsScheduled(root, eventTime);
2642
+ schedulePendingInteractions(root, retryLane);
2643
}
2644
}
2645
2646
export function retryDehydratedSuspenseBoundary(boundaryFiber: Fiber) {
2647
const suspenseState: null | SuspenseState = boundaryFiber.memoizedState;
2813
- let retryTime = NoWork;
2648
+ let retryLane = NoLane;
2649
if (suspenseState !== null) {
2815
- retryTime = suspenseState.retryTime;
2650
+ retryLane = suspenseState.retryLane;
2651
}
2817
- retryTimedOutBoundary(boundaryFiber, retryTime);
2652
+ retryTimedOutBoundary(boundaryFiber, retryLane);
2653
}
2654
2655
export function resolveRetryWakeable(boundaryFiber: Fiber, wakeable: Wakeable) {
2821
- let retryTime = NoWork; // Default
2656
+ let retryLane = NoLane; // Default
2657
let retryCache: WeakSet<Wakeable> | Set<Wakeable> | null;
2658
if (enableSuspenseServerRenderer) {
2659
switch (boundaryFiber.tag) {
2661
retryCache = boundaryFiber.stateNode;
2662
const suspenseState: null | SuspenseState = boundaryFiber.memoizedState;
2663
if (suspenseState !== null) {
2829
- retryTime = suspenseState.retryTime;
2664
+ retryLane = suspenseState.retryLane;
2665
}
2666
break;
2667
case SuspenseListComponent:
2684
retryCache.delete(wakeable);
2685
}
2686
2852
- retryTimedOutBoundary(boundaryFiber, retryTime);
2687
+ retryTimedOutBoundary(boundaryFiber, retryLane);
2688
}
2689
2690
// Computes the next Just Noticeable Difference (JND) boundary.
2713
}
2714
2715
function computeMsUntilSuspenseLoadingDelay(
2881
- mostRecentEventTime: ExpirationTime,
2882
- committedExpirationTime: ExpirationTime,
2716
+ mostRecentEventTime: number,
2717
suspenseConfig: SuspenseConfig,
2718
) {
2719
const busyMinDurationMs = (suspenseConfig.busyMinDurationMs: any) | 0;
2724
2725
// Compute the time until this render pass would expire.
2726
const currentTimeMs: number = now();
2893
- const eventTimeMs: number = inferTimeFromExpirationTimeWithSuspenseConfig(
2894
- mostRecentEventTime,
2895
- suspenseConfig,
2896
- );
2727
+ const eventTimeMs: number = mostRecentEventTime;
2728
const timeElapsed = currentTimeMs - eventTimeMs;
2729
if (timeElapsed <= busyDelayMs) {
2730
// If we haven't yet waited longer than the initial delay, we don't
2905
let beginWork;
2906
if (__DEV__ && replayFailedUnitOfWorkWithInvokeGuardedCallback) {
2907
const dummyFiber = null;
3077
- beginWork = (current, unitOfWork, expirationTime) => {
2908
+ beginWork = (current, unitOfWork, lanes) => {
2909
// If a component throws an error, we replay it again in a synchronously
2910
// dispatched event, so that the debugger will treat it as an uncaught
2911
// error See ReactErrorUtils for more information.
2917
unitOfWork,
2918
);
2919
try {
3089
- return originalBeginWork(current, unitOfWork, expirationTime);
2920
+ return originalBeginWork(current, unitOfWork, lanes);
2921
} catch (originalError) {
2922
if (
2923
originalError !== null &&
2953
null,
2954
current,
2955
unitOfWork,
3125
- expirationTime,
2956
+ lanes,
2957
);
2958
2959
if (hasCaughtError()) {
3167
}
3168
}
3169
3339
-function computeThreadID(root, expirationTime) {
3170
+function computeThreadID(root: FiberRoot, lane: Lane | Lanes) {
3171
// Interaction threads are unique per root and expiration time.
3341
- return expirationTime * 1000 + root.interactionThreadID;
3172
+ // NOTE: Intentionally unsound cast. All that matters is that it's a number
3173
+ // and it represents a batch of work. Could make a helper function instead,
3174
+ // but meh this is fine for now.
3175
+ return (lane: any) * 1000 + root.interactionThreadID;
3176
}
3177
3344
-export function markSpawnedWork(expirationTime: ExpirationTime) {
3178
+export function markSpawnedWork(lane: Lane | Lanes) {
3179
if (!enableSchedulerTracing) {
3180
return;
3181
}
3182
if (spawnedWorkDuringRender === null) {
3349
- spawnedWorkDuringRender = [expirationTime];
3183
+ spawnedWorkDuringRender = [lane];
3184
} else {
3351
- spawnedWorkDuringRender.push(expirationTime);
3185
+ spawnedWorkDuringRender.push(lane);
3186
}
3187
}
3188
3355
-function scheduleInteractions(root, expirationTime, interactions) {
3189
+function scheduleInteractions(
3190
+ root: FiberRoot,
3191
+ lane: Lane | Lanes,
3192
+ interactions: Set<Interaction>,
3193
+) {
3194
if (!enableSchedulerTracing) {
3195
return;
3196
}
3197
3198
if (interactions.size > 0) {
3361
- const pendingInteractionMap = root.pendingInteractionMap_old;
3362
- const pendingInteractions = pendingInteractionMap.get(expirationTime);
3199
+ const pendingInteractionMap = root.pendingInteractionMap;
3200
+ const pendingInteractions = pendingInteractionMap.get(lane);
3201
if (pendingInteractions != null) {
3202
interactions.forEach(interaction => {
3203
if (!pendingInteractions.has(interaction)) {
3208
pendingInteractions.add(interaction);
3209
});
3210
} else {
3373
- pendingInteractionMap.set(expirationTime, new Set(interactions));
3211
+ pendingInteractionMap.set(lane, new Set(interactions));
3212
3213
// Update the pending async work count for the current interactions.
3214
interactions.forEach(interaction => {
3218
3219
const subscriber = __subscriberRef.current;
3220
if (subscriber !== null) {
3383
- const threadID = computeThreadID(root, expirationTime);
3221
+ const threadID = computeThreadID(root, lane);
3222
subscriber.onWorkScheduled(interactions, threadID);
3223
}
3224
}
3225
}
3226
3389
-function schedulePendingInteractions(root, expirationTime) {
3227
+function schedulePendingInteractions(root: FiberRoot, lane: Lane | Lanes) {
3228
// This is called when work is scheduled on a root.
3229
// It associates the current interactions with the newly-scheduled expiration.
3230
// They will be restored when that expiration is later committed.
3232
return;
3233
}
3234
3397
- scheduleInteractions(root, expirationTime, __interactionsRef.current);
3235
+ scheduleInteractions(root, lane, __interactionsRef.current);
3236
}
3237
3400
-function startWorkOnPendingInteractions(root, expirationTime) {
3238
+function startWorkOnPendingInteractions(root: FiberRoot, lanes: Lanes) {
3239
// This is called when new work is started on a root.
3240
if (!enableSchedulerTracing) {
3241
return;
3245
// we can accurately attribute time spent working on it, And so that cascading
3246
// work triggered during the render phase will be associated with it.
3247
const interactions: Set<Interaction> = new Set();
3410
- root.pendingInteractionMap_old.forEach(
3411
- (scheduledInteractions, scheduledExpirationTime) => {
3412
- if (scheduledExpirationTime >= expirationTime) {
3413
- scheduledInteractions.forEach(interaction =>
3414
- interactions.add(interaction),
3415
- );
3416
- }
3417
- },
3418
- );
3248
+ root.pendingInteractionMap.forEach((scheduledInteractions, scheduledLane) => {
3249
+ if (includesSomeLane(lanes, scheduledLane)) {
3250
+ scheduledInteractions.forEach(interaction =>
3251
+ interactions.add(interaction),
3252
+ );
3253
+ }
3254
+ });
3255
3256
// Store the current set of interactions on the FiberRoot for a few reasons:
3257
// We can re-use it in hot functions like performConcurrentWorkOnRoot()
3263
if (interactions.size > 0) {
3264
const subscriber = __subscriberRef.current;
3265
if (subscriber !== null) {
3430
- const threadID = computeThreadID(root, expirationTime);
3266
+ const threadID = computeThreadID(root, lanes);
3267
try {
3268
subscriber.onWorkStarted(interactions, threadID);
3269
} catch (error) {
3270
// If the subscriber throws, rethrow it in a separate task
3435
- scheduleCallback(ImmediatePriority, () => {
3271
+ scheduleCallback(ImmediateSchedulerPriority, () => {
3272
throw error;
3273
});
3274
}
3276
}
3277
}
3278
3443
-function finishPendingInteractions(root, committedExpirationTime) {
3279
+function finishPendingInteractions(root, committedLanes) {
3280
if (!enableSchedulerTracing) {
3281
return;
3282
}
3283
3448
- const earliestRemainingTimeAfterCommit = root.firstPendingTime;
3284
+ const remainingLanesAfterCommit = root.pendingLanes;
3285
3286
let subscriber;
3287
3288
try {
3289
subscriber = __subscriberRef.current;
3290
if (subscriber !== null && root.memoizedInteractions.size > 0) {
3455
- const threadID = computeThreadID(root, committedExpirationTime);
3291
+ // FIXME: More than one lane can finish in a single commit.
3292
+ const threadID = computeThreadID(root, committedLanes);
3293
subscriber.onWorkStopped(root.memoizedInteractions, threadID);
3294
}
3295
} catch (error) {
3296
// If the subscriber throws, rethrow it in a separate task
3460
- scheduleCallback(ImmediatePriority, () => {
3297
+ scheduleCallback(ImmediateSchedulerPriority, () => {
3298
throw error;
3299
});
3300
} finally {
3301
// Clear completed interactions from the pending Map.
3302
// Unless the render was suspended or cascading work was scheduled,
3303
// In which case– leave pending interactions until the subsequent render.
3467
- const pendingInteractionMap = root.pendingInteractionMap_old;
3468
- pendingInteractionMap.forEach(
3469
- (scheduledInteractions, scheduledExpirationTime) => {
3470
- // Only decrement the pending interaction count if we're done.
3471
- // If there's still work at the current priority,
3472
- // That indicates that we are waiting for suspense data.
3473
- if (scheduledExpirationTime > earliestRemainingTimeAfterCommit) {
3474
- pendingInteractionMap.delete(scheduledExpirationTime);
3475
-
3476
- scheduledInteractions.forEach(interaction => {
3477
- interaction.__count--;
3478
-
3479
- if (subscriber !== null && interaction.__count === 0) {
3480
- try {
3481
- subscriber.onInteractionScheduledWorkCompleted(interaction);
3482
- } catch (error) {
3483
- // If the subscriber throws, rethrow it in a separate task
3484
- scheduleCallback(ImmediatePriority, () => {
3485
- throw error;
3486
- });
3487
- }
3304
+ const pendingInteractionMap = root.pendingInteractionMap;
3305
+ pendingInteractionMap.forEach((scheduledInteractions, lane) => {
3306
+ // Only decrement the pending interaction count if we're done.
3307
+ // If there's still work at the current priority,
3308
+ // That indicates that we are waiting for suspense data.
3309
+ if (!includesSomeLane(remainingLanesAfterCommit, lane)) {
3310
+ pendingInteractionMap.delete(lane);
3311
+
3312
+ scheduledInteractions.forEach(interaction => {
3313
+ interaction.__count--;
3314
+
3315
+ if (subscriber !== null && interaction.__count === 0) {
3316
+ try {
3317
+ subscriber.onInteractionScheduledWorkCompleted(interaction);
3318
+ } catch (error) {
3319
+ // If the subscriber throws, rethrow it in a separate task
3320
+ scheduleCallback(ImmediateSchedulerPriority, () => {
3321
+ throw error;
3322
+ });
3323
}
3489
- });
3490
- }
3491
- },
3492
- );
3324
+ }
3325
+ });
3326
+ }
3327
+ });
3328
}
3329
}
3330