WorkPhase -> ExecutionContext
WorkPhase is an enum that represents the currently executing phase of the React update -> render -> commit cycle. However, in practice, it's hard to use because different "phases" can be nested inside each other. For example, the commit phase can be nested inside the "batched phase." This replaces WorkPhase with a different concept: ExecutionContext. ExecutionContext is a bitmask instead of an enum. It represents a stack of React entry points. For example, when `batchedUpdates` is called from inside an effect, the ExecutionContext is `BatchedContext | CommitContext`.
Andrew Clark committed
May 31, 2019 at 14:13 UTC
73c380fca77a32af167d85c77553ae5d40c6fda8
1 file changed
+79
-80
packages/react-reconciler/src/ReactFiberWorkLoop.js
+79
-80
@@ -176,14 +176,14 @@ const {
176
ReactCurrentActingRendererSigil,
177
} = ReactSharedInternals;
178
179
-type WorkPhase = 0 | 1 | 2 | 3 | 4 | 5 | 6;
180
-const NotWorking = 0;
181
-const BatchedPhase = 1;
182
-const LegacyUnbatchedPhase = 2;
183
-const FlushSyncPhase = 3;
184
-const RenderPhase = 4;
185
-const CommitPhase = 5;
186
-const BatchedEventPhase = 6;
179
+type ExecutionContext = number;
180
+
181
+const NoContext = /* */ 0b00000;
182
+const BatchedContext = /* */ 0b00001;
183
+const EventContext = /* */ 0b00010;
184
+const LegacyUnbatchedContext = /* */ 0b00100;
185
+const RenderContext = /* */ 0b01000;
186
+const CommitContext = /* */ 0b10000;
187
188
type RootExitStatus = 0 | 1 | 2 | 3 | 4;
189
const RootIncomplete = 0;
@@ -196,8 +196,8 @@ export type Thenable = {
196
then(resolve: () => mixed, reject?: () => mixed): Thenable | void,
197
};
198
199
-// The phase of work we're currently in
200
-let workPhase: WorkPhase = NotWorking;
199
+// Describes where we are in the React execution stack
200
+let executionContext: ExecutionContext = NoContext;
201
// The root we're working on
202
let workInProgressRoot: FiberRoot | null = null;
203
// The fiber we're working on
@@ -257,7 +257,7 @@ let interruptedBy: Fiber | null = null;
257
let currentEventTime: ExpirationTime = NoWork;
258
259
export function requestCurrentTime() {
260
- if (workPhase === RenderPhase || workPhase === CommitPhase) {
260
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
261
// We're inside React, so it's fine to read the actual time.
262
return msToExpirationTime(now());
263
}
@@ -286,7 +286,7 @@ export function computeExpirationForFiber(
286
return priorityLevel === ImmediatePriority ? Sync : Batched;
287
}
288
289
- if (workPhase === RenderPhase) {
289
+ if ((executionContext & RenderContext) !== NoContext) {
290
// Use whatever time we're already rendering
291
return renderExpirationTime;
292
}
@@ -364,7 +364,12 @@ export function scheduleUpdateOnFiber(
364
recordScheduleUpdate();
365
366
if (expirationTime === Sync) {
367
- if (workPhase === LegacyUnbatchedPhase) {
367
+ if (
368
+ // Check if we're inside unbatchedUpdates
369
+ (executionContext & LegacyUnbatchedContext) !== NoContext &&
370
+ // Check if we're not already rendering
371
+ (executionContext & (RenderContext | CommitContext)) === NoContext
372
+ ) {
373
// Register pending interactions on the root to avoid losing traced interaction data.
374
schedulePendingInteraction(root, expirationTime);
375
@@ -377,7 +382,7 @@ export function scheduleUpdateOnFiber(
382
}
383
} else {
384
scheduleCallbackForRoot(root, ImmediatePriority, Sync);
380
- if (workPhase === NotWorking) {
385
+ if (executionContext === NoContext) {
386
// Flush the synchronous work now, wnless we're already working or inside
387
// a batch. This is intentionally inside scheduleUpdateOnFiber instead of
388
// scheduleCallbackForFiber to preserve the ability to schedule a callback
@@ -518,8 +523,7 @@ function scheduleCallbackForRoot(
523
if (
524
enableUserTimingAPI &&
525
expirationTime !== Sync &&
521
- workPhase !== RenderPhase &&
522
- workPhase !== CommitPhase
526
+ (executionContext & (RenderContext | CommitContext)) === NoContext
527
) {
528
// Scheduled an async callback, and we're not already working. Add an
529
// entry to the flamegraph that shows we're waiting for a callback
@@ -557,7 +561,7 @@ function runRootCallback(root, callback, isSync) {
561
}
562
563
export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
560
- if (workPhase === RenderPhase || workPhase === CommitPhase) {
564
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
565
invariant(
566
false,
567
'work.commit(): Cannot commit while already rendering. This likely ' +
@@ -569,22 +573,24 @@ export function flushRoot(root: FiberRoot, expirationTime: ExpirationTime) {
573
}
574
575
export function flushDiscreteUpdates() {
572
- // TODO: we ideally do not want to early reurn for BatchedPhase here either.
573
- // Removing this causes act() tests to fail, so we should follow up.
574
- if (workPhase === CommitPhase || workPhase === BatchedPhase) {
575
- // We're inside the commit phase or batched phase, so we can't
576
- // synchronously flush pending work. This is probably a nested event
577
- // dispatch triggered by a lifecycle/effect, like `el.focus()`. Exit.
578
- return;
579
- }
580
- if (workPhase === RenderPhase) {
581
- if (__DEV__) {
576
+ // TODO: Should be able to flush inside batchedUpdates, but not inside `act`.
577
+ // However, `act` uses `batchedUpdates`, so there's no way to distinguish
578
+ // those two cases. Need to fix this before exposing flushDiscreteUpdates
579
+ // as a public API.
580
+ if (
581
+ (executionContext & (BatchedContext | RenderContext | CommitContext)) !==
582
+ NoContext
583
+ ) {
584
+ if (__DEV__ && (executionContext & RenderContext) !== NoContext) {
585
warning(
586
false,
587
'unstable_flushDiscreteUpdates: Cannot flush updates when React is ' +
588
'already rendering.',
589
);
590
}
591
+ // We're already rendering, so we can't synchronously flush pending work.
592
+ // This is probably a nested event dispatch triggered by a lifecycle/effect,
593
+ // like `el.focus()`. Exit.
594
return;
595
}
596
flushPendingDiscreteUpdates();
@@ -650,69 +656,62 @@ function flushPendingDiscreteUpdates() {
656
}
657
658
export function batchedUpdates<A, R>(fn: A => R, a: A): R {
653
- if (workPhase !== NotWorking) {
654
- // We're already working, or inside a batch, so batchedUpdates is a no-op.
655
- return fn(a);
656
- }
657
- workPhase = BatchedPhase;
659
+ const prevExecutionContext = executionContext;
660
+ executionContext |= BatchedContext;
661
try {
662
return fn(a);
663
} finally {
661
- workPhase = NotWorking;
662
- // Flush the immediate callbacks that were scheduled during this batch
663
- flushSyncCallbackQueue();
664
+ executionContext = prevExecutionContext;
665
+ if (executionContext === NoContext) {
666
+ // Flush the immediate callbacks that were scheduled during this batch
667
+ flushSyncCallbackQueue();
668
+ }
669
}
670
}
671
672
export function batchedEventUpdates<A, R>(fn: A => R, a: A): R {
668
- if (workPhase !== NotWorking) {
669
- // We're already working, or inside a batch, so batchedUpdates is a no-op.
670
- return fn(a);
671
- }
672
- const prevWorkPhase = workPhase;
673
- workPhase = BatchedEventPhase;
673
+ const prevExecutionContext = executionContext;
674
+ executionContext |= EventContext;
675
try {
676
return fn(a);
677
} finally {
677
- workPhase = prevWorkPhase;
678
- // Flush the immediate callbacks that were scheduled during this batch
679
- flushSyncCallbackQueue();
678
+ executionContext = prevExecutionContext;
679
+ if (executionContext === NoContext) {
680
+ // Flush the immediate callbacks that were scheduled during this batch
681
+ flushSyncCallbackQueue();
682
+ }
683
}
684
}
685
686
export function unbatchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
684
- if (
685
- workPhase !== BatchedPhase &&
686
- workPhase !== FlushSyncPhase &&
687
- workPhase !== BatchedEventPhase
688
- ) {
689
- // We're not inside batchedUpdates or flushSync, so unbatchedUpdates is
690
- // a no-op.
691
- return fn(a);
692
- }
693
- const prevWorkPhase = workPhase;
694
- workPhase = LegacyUnbatchedPhase;
687
+ const prevExecutionContext = executionContext;
688
+ executionContext &= ~BatchedContext;
689
+ executionContext |= LegacyUnbatchedContext;
690
try {
691
return fn(a);
692
} finally {
698
- workPhase = prevWorkPhase;
693
+ executionContext = prevExecutionContext;
694
+ if (executionContext === NoContext) {
695
+ // Flush the immediate callbacks that were scheduled during this batch
696
+ flushSyncCallbackQueue();
697
+ }
698
}
699
}
700
701
export function flushSync<A, R>(fn: A => R, a: A): R {
703
- if (workPhase === RenderPhase || workPhase === CommitPhase) {
702
+ if ((executionContext & (RenderContext | CommitContext)) !== NoContext) {
703
invariant(
704
false,
705
'flushSync was called from inside a lifecycle method. It cannot be ' +
706
'called when React is already rendering.',
707
);
708
}
710
- const prevWorkPhase = workPhase;
711
- workPhase = FlushSyncPhase;
709
+ const prevExecutionContext = executionContext;
710
+ executionContext |= BatchedContext;
711
try {
712
return runWithPriority(ImmediatePriority, fn.bind(null, a));
713
} finally {
715
- workPhase = prevWorkPhase;
714
+ executionContext = prevExecutionContext;
715
// Flush the immediate callbacks that were scheduled during this batch.
716
// Note that this will happen even if batchedUpdates is higher up
717
// the stack.
@@ -721,13 +720,13 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
720
}
721
722
export function flushControlled(fn: () => mixed): void {
724
- const prevWorkPhase = workPhase;
725
- workPhase = BatchedPhase;
723
+ const prevExecutionContext = executionContext;
724
+ executionContext |= BatchedContext;
725
try {
726
runWithPriority(ImmediatePriority, fn);
727
} finally {
729
- workPhase = prevWorkPhase;
730
- if (workPhase === NotWorking) {
728
+ executionContext = prevExecutionContext;
729
+ if (executionContext === NoContext) {
730
// Flush the immediate callbacks that were scheduled during this batch
731
flushSyncCallbackQueue();
732
}
@@ -775,7 +774,7 @@ function renderRoot(
774
isSync: boolean,
775
): SchedulerCallback | null {
776
invariant(
778
- workPhase !== RenderPhase && workPhase !== CommitPhase,
777
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
778
'Should not already be working.',
779
);
780
@@ -828,8 +827,8 @@ function renderRoot(
827
// If we have a work-in-progress fiber, it means there's still work to do
828
// in this root.
829
if (workInProgress !== null) {
831
- const prevWorkPhase = workPhase;
832
- workPhase = RenderPhase;
830
+ const prevExecutionContext = executionContext;
831
+ executionContext |= RenderContext;
832
let prevDispatcher = ReactCurrentDispatcher.current;
833
if (prevDispatcher === null) {
834
// The React isomorphic package does not include a default dispatcher.
@@ -855,7 +854,7 @@ function renderRoot(
854
const currentTime = requestCurrentTime();
855
if (currentTime < expirationTime) {
856
// Restart at the current time.
858
- workPhase = prevWorkPhase;
857
+ executionContext = prevExecutionContext;
858
resetContextDependencies();
859
ReactCurrentDispatcher.current = prevDispatcher;
860
if (enableSchedulerTracing) {
@@ -892,7 +891,7 @@ function renderRoot(
891
// supposed to capture all errors that weren't caught by an error
892
// boundary.
893
prepareFreshStack(root, expirationTime);
895
- workPhase = prevWorkPhase;
894
+ executionContext = prevExecutionContext;
895
throw thrownValue;
896
}
897
@@ -915,7 +914,7 @@ function renderRoot(
914
}
915
} while (true);
916
918
- workPhase = prevWorkPhase;
917
+ executionContext = prevExecutionContext;
918
resetContextDependencies();
919
ReactCurrentDispatcher.current = prevDispatcher;
920
if (enableSchedulerTracing) {
@@ -1454,7 +1453,7 @@ function commitRootImpl(root) {
1453
flushSuspensePriorityWarningInDEV();
1454
1455
invariant(
1457
- workPhase !== RenderPhase && workPhase !== CommitPhase,
1456
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
1457
'Should not already be working.',
1458
);
1459
@@ -1524,8 +1523,8 @@ function commitRootImpl(root) {
1523
}
1524
1525
if (firstEffect !== null) {
1527
- const prevWorkPhase = workPhase;
1528
- workPhase = CommitPhase;
1526
+ const prevExecutionContext = executionContext;
1527
+ executionContext |= CommitContext;
1528
let prevInteractions: Set<Interaction> | null = null;
1529
if (enableSchedulerTracing) {
1530
prevInteractions = __interactionsRef.current;
@@ -1640,7 +1639,7 @@ function commitRootImpl(root) {
1639
if (enableSchedulerTracing) {
1640
__interactionsRef.current = ((prevInteractions: any): Set<Interaction>);
1641
}
1643
- workPhase = prevWorkPhase;
1642
+ executionContext = prevExecutionContext;
1643
} else {
1644
// No effects.
1645
root.current = finishedWork;
@@ -1712,7 +1711,7 @@ function commitRootImpl(root) {
1711
throw error;
1712
}
1713
1715
- if (workPhase === LegacyUnbatchedPhase) {
1714
+ if ((executionContext & LegacyUnbatchedContext) !== NoContext) {
1715
// This is a legacy edge case. We just committed the initial mount of
1716
// a ReactDOM.render-ed root inside of batchedUpdates. The commit fired
1717
// synchronously, but layout updates should be deferred until the end
@@ -1855,11 +1854,11 @@ export function flushPassiveEffects() {
1854
}
1855
1856
invariant(
1858
- workPhase !== RenderPhase && workPhase !== CommitPhase,
1857
+ (executionContext & (RenderContext | CommitContext)) === NoContext,
1858
'Cannot flush passive effects while already rendering.',
1859
);
1861
- const prevWorkPhase = workPhase;
1862
- workPhase = CommitPhase;
1860
+ const prevExecutionContext = executionContext;
1861
+ executionContext |= CommitContext;
1862
1863
// Note: This currently assumes there are no passive effects on the root
1864
// fiber, because the root is not part of its own effect list. This could
@@ -1891,7 +1890,7 @@ export function flushPassiveEffects() {
1890
finishPendingInteractions(root, expirationTime);
1891
}
1892
1894
- workPhase = prevWorkPhase;
1893
+ executionContext = prevExecutionContext;
1894
flushSyncCallbackQueue();
1895
1896
// If additional passive effects were scheduled, increment a counter. If this
@@ -2400,7 +2399,7 @@ export function warnIfNotScopedWithMatchingAct(fiber: Fiber): void {
2399
function warnIfNotCurrentlyActingUpdatesInDEV(fiber: Fiber): void {
2400
if (__DEV__) {
2401
if (
2403
- workPhase === NotWorking &&
2402
+ executionContext === NoContext &&
2403
ReactCurrentActingRendererSigil.current !== ReactActingRendererSigil
2404
) {
2405
warningWithoutStack(