@samitouri / QOS-React-2 / commits / 73c380fca7

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(