@samitouri / QOS-React-2 / commits / eae90cdbe9

Effects list refactor continued: passive effects traversal (#19374)

* Adds new `Passive` subtree tag value. * Adds recursive traversal for passive effects (mounts and unmounts). * Removes `pendingPassiveHookEffectsMount` and `pendingPassiveHookEffectsUnmount` arrays from work loop. * Re-adds sibling and child pointer detaching (temporarily removed in previous PR). * Addresses some minor TODO comments left over from previous PRs. --- Co-authored-by: Luna Ruan <luna@fb.com>

Brian Vaughn committed Jul 29, 2020 at 16:54 UTC eae90cdbe90ab6ad8c48cee6f5af10e767a3f9fc
11 files changed +458 -274
packages/react-reconciler/src/ReactChildFiber.new.js
+1 -1
@@ -15,7 +15,7 @@ import type {Fiber} from './ReactInternalTypes';
15 import type {Lanes} from './ReactFiberLane';
16
17 import getComponentName from 'shared/getComponentName';
18 -import {Placement, Deletion} from './ReactSideEffectTags';
18 +import {Deletion, Placement} from './ReactSideEffectTags';
19 import {
20 getIteratorFn,
21 REACT_ELEMENT_TYPE,
packages/react-reconciler/src/ReactFiber.new.js
+4 -3
@@ -29,7 +29,7 @@ import {
29 enableScopeAPI,
30 enableBlocksAPI,
31 } from 'shared/ReactFeatureFlags';
32 -import {NoEffect, Placement} from './ReactSideEffectTags';
32 +import {NoEffect, Placement, StaticMask} from './ReactSideEffectTags';
33 import {NoEffect as NoSubtreeEffect} from './ReactSubtreeTags';
34 import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
35 import {
@@ -288,8 +288,6 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
288 workInProgress.type = current.type;
289
290 // We already have an alternate.
291 - // Reset the effect tag.
292 - workInProgress.effectTag = NoEffect;
291 workInProgress.subtreeTag = NoSubtreeEffect;
292 workInProgress.deletions = null;
293
@@ -308,6 +306,9 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
306 }
307 }
308
309 + // Reset all effects except static ones.
310 + // Static effects are not specific to a render.
311 + workInProgress.effectTag = current.effectTag & StaticMask;
312 workInProgress.childLanes = current.childLanes;
313 workInProgress.lanes = current.lanes;
314
packages/react-reconciler/src/ReactFiberBeginWork.new.js
-1
@@ -2064,7 +2064,6 @@ function updateSuspensePrimaryChildren(
2064 if (currentFallbackChildFragment !== null) {
2065 // Delete the fallback child fragment
2066 currentFallbackChildFragment.nextEffect = null;
2067 - currentFallbackChildFragment.effectTag = Deletion;
2067 workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment;
2068 const deletions = workInProgress.deletions;
2069 if (deletions === null) {
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+31 -45
@@ -68,6 +68,7 @@ import {
68 Placement,
69 Snapshot,
70 Update,
71 + Passive,
72 } from './ReactSideEffectTags';
73 import getComponentName from 'shared/getComponentName';
74 import invariant from 'shared/invariant';
@@ -115,9 +116,8 @@ import {
116 captureCommitPhaseError,
117 resolveRetryWakeable,
118 markCommitTimeOfFallback,
118 - enqueuePendingPassiveHookEffectMount,
119 - enqueuePendingPassiveHookEffectUnmount,
119 enqueuePendingPassiveProfilerEffect,
120 + schedulePassiveEffectCallback,
121 } from './ReactFiberWorkLoop.new';
122 import {
123 NoEffect as NoHookEffect,
@@ -130,6 +130,10 @@ import {
130 updateDeprecatedEventListeners,
131 unmountDeprecatedResponderListeners,
132 } from './ReactFiberDeprecatedEvents.new';
133 +import {
134 + NoEffect as NoSubtreeTag,
135 + Passive as PassiveSubtreeTag,
136 +} from './ReactSubtreeTags';
137
138 let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
139 if (__DEV__) {
@@ -381,26 +385,6 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
385 }
386 }
387
384 -function schedulePassiveEffects(finishedWork: Fiber) {
385 - const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
386 - const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
387 - if (lastEffect !== null) {
388 - const firstEffect = lastEffect.next;
389 - let effect = firstEffect;
390 - do {
391 - const {next, tag} = effect;
392 - if (
393 - (tag & HookPassive) !== NoHookEffect &&
394 - (tag & HookHasEffect) !== NoHookEffect
395 - ) {
396 - enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
397 - enqueuePendingPassiveHookEffectMount(finishedWork, effect);
398 - }
399 - effect = next;
400 - } while (effect !== firstEffect);
401 - }
402 -}
403 -
388 export function commitPassiveEffectDurations(
389 finishedRoot: FiberRoot,
390 finishedWork: Fiber,
@@ -486,7 +470,9 @@ function commitLifeCycles(
470 commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
471 }
472
489 - schedulePassiveEffects(finishedWork);
473 + if ((finishedWork.subtreeTag & PassiveSubtreeTag) !== NoSubtreeTag) {
474 + schedulePassiveEffectCallback();
475 + }
476 return;
477 }
478 case ClassComponent: {
@@ -892,7 +878,12 @@ function commitUnmount(
878 const {destroy, tag} = effect;
879 if (destroy !== undefined) {
880 if ((tag & HookPassive) !== NoHookEffect) {
895 - enqueuePendingPassiveHookEffectUnmount(current, effect);
881 + // TODO: Consider if we can move this block out of the synchronous commit phase
882 + effect.tag |= HookHasEffect;
883 +
884 + current.effectTag |= Passive;
885 +
886 + schedulePassiveEffectCallback();
887 } else {
888 if (
889 enableProfilerTimer &&
@@ -1013,29 +1004,24 @@ function commitNestedUnmounts(
1004 }
1005
1006 function detachFiberMutation(fiber: Fiber) {
1016 - // Cut off the return pointers to disconnect it from the tree. Ideally, we
1017 - // should clear the child pointer of the parent alternate to let this
1007 + // Cut off the return pointer to disconnect it from the tree.
1008 + // This enables us to detect and warn against state updates on an unmounted component.
1009 + // It also prevents events from bubbling from within disconnected components.
1010 + //
1011 + // Ideally, we should also clear the child pointer of the parent alternate to let this
1012 // get GC:ed but we don't know which for sure which parent is the current
1019 - // one so we'll settle for GC:ing the subtree of this child. This child
1020 - // itself will be GC:ed when the parent updates the next time.
1021 - // Note: we cannot null out sibling here, otherwise it can cause issues
1022 - // with findDOMNode and how it requires the sibling field to carry out
1023 - // traversal in a later effect. See PR #16820. We now clear the sibling
1024 - // field after effects, see: detachFiberAfterEffects.
1025 - fiber.alternate = null;
1026 - fiber.child = null;
1027 - fiber.dependencies = null;
1028 - fiber.firstEffect = null;
1029 - fiber.lastEffect = null;
1030 - fiber.memoizedProps = null;
1031 - fiber.memoizedState = null;
1032 - fiber.pendingProps = null;
1033 - fiber.return = null;
1034 - fiber.stateNode = null;
1035 - fiber.updateQueue = null;
1036 - if (__DEV__) {
1037 - fiber._debugOwner = null;
1013 + // one so we'll settle for GC:ing the subtree of this child.
1014 + // This child itself will be GC:ed when the parent updates the next time.
1015 + //
1016 + // Note that we can't clear child or sibling pointers yet.
1017 + // They're needed for passive effects and for findDOMNode.
1018 + // We defer those fields, and all other cleanup, to the passive phase (see detachFiberAfterEffects).
1019 + const alternate = fiber.alternate;
1020 + if (alternate !== null) {
1021 + alternate.return = null;
1022 + fiber.alternate = null;
1023 }
1024 + fiber.return = null;
1025 }
1026
1027 function emptyPortalContainer(current: Fiber) {
packages/react-reconciler/src/ReactFiberHooks.new.js
+4 -2
@@ -50,6 +50,7 @@ import {createDeprecatedResponderListener} from './ReactFiberDeprecatedEvents.ne
50 import {
51 Update as UpdateEffect,
52 Passive as PassiveEffect,
53 + PassiveStatic as PassiveStaticEffect,
54 } from './ReactSideEffectTags';
55 import {
56 HasEffect as HookHasEffect,
@@ -1270,7 +1271,7 @@ function mountEffect(
1271 }
1272 }
1273 return mountEffectImpl(
1273 - UpdateEffect | PassiveEffect,
1274 + UpdateEffect | PassiveEffect | PassiveStaticEffect,
1275 HookPassive,
1276 create,
1277 deps,
@@ -1631,7 +1632,8 @@ function mountOpaqueIdentifier(): OpaqueIDType | void {
1632 const setId = mountState(id)[1];
1633
1634 if ((currentlyRenderingFiber.mode & BlockingMode) === NoMode) {
1634 - currentlyRenderingFiber.effectTag |= UpdateEffect | PassiveEffect;
1635 + currentlyRenderingFiber.effectTag |=
1636 + UpdateEffect | PassiveEffect | PassiveStaticEffect;
1637 pushEffect(
1638 HookHasEffect | HookPassive,
1639 () => {
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+1 -1
@@ -124,7 +124,7 @@ function deleteHydratableInstance(
124 const childToDelete = createFiberFromHostInstanceForDeletion();
125 childToDelete.stateNode = instance;
126 childToDelete.return = returnFiber;
127 - childToDelete.effectTag = Deletion;
127 +
128 const deletions = returnFiber.deletions;
129 if (deletions === null) {
130 returnFiber.deletions = [childToDelete];
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+328 -172
@@ -16,6 +16,7 @@ import type {SuspenseConfig} from './ReactFiberSuspenseConfig';
16 import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
17 import type {Effect as HookEffect} from './ReactFiberHooks.new';
18 import type {StackCursor} from './ReactFiberStack.new';
19 +import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
20
21 import {
22 warnAboutDeprecatedLifecycles,
@@ -49,6 +50,11 @@ import {
50 flushSyncCallbackQueue,
51 scheduleSyncCallback,
52 } from './SchedulerWithReactIntegration.new';
53 +import {
54 + NoEffect as NoHookEffect,
55 + HasEffect as HookHasEffect,
56 + Passive as HookPassive,
57 +} from './ReactHookEffectTags';
58 import {
59 logCommitStarted,
60 logCommitStopped,
@@ -126,7 +132,7 @@ import {
132 Snapshot,
133 Callback,
134 Passive,
129 - PassiveUnmountPendingDev,
135 + PassiveStatic,
136 Incomplete,
137 HostEffectMask,
138 Hydrating,
@@ -134,12 +140,14 @@ import {
140 BeforeMutationMask,
141 MutationMask,
142 LayoutMask,
143 + PassiveMask,
144 } from './ReactSideEffectTags';
145 import {
146 NoEffect as NoSubtreeTag,
140 - BeforeMutation,
141 - Mutation,
142 - Layout,
147 + BeforeMutation as BeforeMutationSubtreeTag,
148 + Mutation as MutationSubtreeTag,
149 + Layout as LayoutSubtreeTag,
150 + Passive as PassiveSubtreeTag,
151 } from './ReactSubtreeTags';
152 import {
153 NoLanePriority,
@@ -327,8 +335,6 @@ let rootDoesHavePassiveEffects: boolean = false;
335 let rootWithPendingPassiveEffects: FiberRoot | null = null;
336 let pendingPassiveEffectsRenderPriority: ReactPriorityLevel = NoSchedulerPriority;
337 let pendingPassiveEffectsLanes: Lanes = NoLanes;
330 -let pendingPassiveHookEffectsMount: Array<HookEffect | Fiber> = [];
331 -let pendingPassiveHookEffectsUnmount: Array<HookEffect | Fiber> = [];
338 let pendingPassiveProfilerEffects: Array<Fiber> = [];
339
340 let rootsWithPendingDiscreteUpdates: Set<FiberRoot> | null = null;
@@ -1882,13 +1888,16 @@ function resetChildLanes(completedWork: Fiber) {
1888
1889 const effectTag = child.effectTag;
1890 if ((effectTag & BeforeMutationMask) !== NoEffect) {
1885 - subtreeTag |= BeforeMutation;
1891 + subtreeTag |= BeforeMutationSubtreeTag;
1892 }
1893 if ((effectTag & MutationMask) !== NoEffect) {
1888 - subtreeTag |= Mutation;
1894 + subtreeTag |= MutationSubtreeTag;
1895 }
1896 if ((effectTag & LayoutMask) !== NoEffect) {
1891 - subtreeTag |= Layout;
1897 + subtreeTag |= LayoutSubtreeTag;
1898 + }
1899 + if ((effectTag & PassiveMask) !== NoEffect) {
1900 + subtreeTag |= PassiveSubtreeTag;
1901 }
1902
1903 // When a fiber is cloned, its actualDuration is reset to 0. This value will
@@ -1929,13 +1938,16 @@ function resetChildLanes(completedWork: Fiber) {
1938
1939 const effectTag = child.effectTag;
1940 if ((effectTag & BeforeMutationMask) !== NoEffect) {
1932 - subtreeTag |= BeforeMutation;
1941 + subtreeTag |= BeforeMutationSubtreeTag;
1942 }
1943 if ((effectTag & MutationMask) !== NoEffect) {
1935 - subtreeTag |= Mutation;
1944 + subtreeTag |= MutationSubtreeTag;
1945 }
1946 if ((effectTag & LayoutMask) !== NoEffect) {
1938 - subtreeTag |= Layout;
1947 + subtreeTag |= LayoutSubtreeTag;
1948 + }
1949 + if ((effectTag & PassiveMask) !== NoEffect) {
1950 + subtreeTag |= PassiveSubtreeTag;
1951 }
1952
1953 child = child.sibling;
@@ -2170,6 +2182,17 @@ function commitRootImpl(root, renderPriorityLevel) {
2182 markLayoutEffectsStopped();
2183 }
2184
2185 + // If there are pending passive effects, schedule a callback to process them.
2186 + if ((finishedWork.subtreeTag & PassiveSubtreeTag) !== NoSubtreeTag) {
2187 + if (!rootDoesHavePassiveEffects) {
2188 + rootDoesHavePassiveEffects = true;
2189 + scheduleCallback(NormalSchedulerPriority, () => {
2190 + flushPassiveEffects();
2191 + return null;
2192 + });
2193 + }
2194 + }
2195 +
2196 // Tell Scheduler to yield at the end of the frame, so the browser has an
2197 // opportunity to paint.
2198 requestPaint();
@@ -2201,8 +2224,6 @@ function commitRootImpl(root, renderPriorityLevel) {
2224 rootWithPendingPassiveEffects = root;
2225 pendingPassiveEffectsLanes = lanes;
2226 pendingPassiveEffectsRenderPriority = renderPriorityLevel;
2204 - } else {
2205 - // TODO (effects) Detach sibling pointers for deleted Fibers
2227 }
2228
2229 // Read this again, since an effect might have updated it
@@ -2312,7 +2333,7 @@ function commitBeforeMutationEffects(firstChild: Fiber) {
2333 }
2334
2335 if (fiber.child !== null) {
2315 - const primarySubtreeTag = fiber.subtreeTag & BeforeMutation;
2336 + const primarySubtreeTag = fiber.subtreeTag & BeforeMutationSubtreeTag;
2337 if (primarySubtreeTag !== NoSubtreeTag) {
2338 commitBeforeMutationEffects(fiber.child);
2339 }
@@ -2396,19 +2417,13 @@ function commitMutationEffects(
2417 ) {
2418 let fiber = firstChild;
2419 while (fiber !== null) {
2399 - if (fiber.deletions !== null) {
2400 - commitMutationEffectsDeletions(
2401 - fiber.deletions,
2402 - root,
2403 - renderPriorityLevel,
2404 - );
2405 -
2406 - // TODO (effects) Don't clear this yet; we may need to cleanup passive effects
2407 - fiber.deletions = null;
2420 + const deletions = fiber.deletions;
2421 + if (deletions !== null) {
2422 + commitMutationEffectsDeletions(deletions, root, renderPriorityLevel);
2423 }
2424
2425 if (fiber.child !== null) {
2411 - const primarySubtreeTag = fiber.subtreeTag & Mutation;
2426 + const primarySubtreeTag = fiber.subtreeTag & MutationSubtreeTag;
2427 if (primarySubtreeTag !== NoSubtreeTag) {
2428 commitMutationEffects(fiber.child, root, renderPriorityLevel);
2429 }
@@ -2538,7 +2553,16 @@ function commitMutationEffectsDeletions(
2553 captureCommitPhaseError(childToDelete, error);
2554 }
2555 }
2541 - // Don't clear the Deletion effect yet; we also use it to know when we need to detach refs later.
2556 + }
2557 +}
2558 +
2559 +export function schedulePassiveEffectCallback() {
2560 + if (!rootDoesHavePassiveEffects) {
2561 + rootDoesHavePassiveEffects = true;
2562 + scheduleCallback(NormalSchedulerPriority, () => {
2563 + flushPassiveEffects();
2564 + return null;
2565 + });
2566 }
2567 }
2568
@@ -2550,7 +2574,7 @@ function commitLayoutEffects(
2574 let fiber = firstChild;
2575 while (fiber !== null) {
2576 if (fiber.child !== null) {
2553 - const primarySubtreeTag = fiber.subtreeTag & Layout;
2577 + const primarySubtreeTag = fiber.subtreeTag & LayoutSubtreeTag;
2578 if (primarySubtreeTag !== NoSubtreeTag) {
2579 commitLayoutEffects(fiber.child, root, committedLanes);
2580 }
@@ -2645,44 +2669,251 @@ export function enqueuePendingPassiveProfilerEffect(fiber: Fiber): void {
2669 }
2670 }
2671
2648 -export function enqueuePendingPassiveHookEffectMount(
2649 - fiber: Fiber,
2650 - effect: HookEffect,
2651 -): void {
2652 - pendingPassiveHookEffectsMount.push(effect, fiber);
2653 - if (!rootDoesHavePassiveEffects) {
2654 - rootDoesHavePassiveEffects = true;
2655 - scheduleCallback(NormalSchedulerPriority, () => {
2656 - flushPassiveEffects();
2657 - return null;
2658 - });
2672 +function invokePassiveEffectCreate(effect: HookEffect): void {
2673 + const create = effect.create;
2674 + effect.destroy = create();
2675 +}
2676 +
2677 +function flushPassiveMountEffects(firstChild: Fiber): void {
2678 + let fiber = firstChild;
2679 + while (fiber !== null) {
2680 + const primarySubtreeTag = fiber.subtreeTag & PassiveSubtreeTag;
2681 +
2682 + if (fiber.child !== null && primarySubtreeTag !== NoSubtreeTag) {
2683 + flushPassiveMountEffects(fiber.child);
2684 + }
2685 +
2686 + if ((fiber.effectTag & Update) !== NoEffect) {
2687 + switch (fiber.tag) {
2688 + case FunctionComponent:
2689 + case ForwardRef:
2690 + case SimpleMemoComponent:
2691 + case Block: {
2692 + flushPassiveMountEffectsImpl(fiber);
2693 + }
2694 + }
2695 + }
2696 +
2697 + fiber = fiber.sibling;
2698 }
2699 }
2700
2662 -export function enqueuePendingPassiveHookEffectUnmount(
2663 - fiber: Fiber,
2664 - effect: HookEffect,
2665 -): void {
2666 - pendingPassiveHookEffectsUnmount.push(effect, fiber);
2667 - if (__DEV__) {
2668 - fiber.effectTag |= PassiveUnmountPendingDev;
2669 - const alternate = fiber.alternate;
2670 - if (alternate !== null) {
2671 - alternate.effectTag |= PassiveUnmountPendingDev;
2701 +function flushPassiveMountEffectsImpl(fiber: Fiber): void {
2702 + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any);
2703 + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
2704 + if (lastEffect !== null) {
2705 + const firstEffect = lastEffect.next;
2706 + let effect = firstEffect;
2707 + do {
2708 + const {next, tag} = effect;
2709 +
2710 + if (
2711 + (tag & HookPassive) !== NoHookEffect &&
2712 + (tag & HookHasEffect) !== NoHookEffect
2713 + ) {
2714 + if (__DEV__) {
2715 + setCurrentDebugFiberInDEV(fiber);
2716 + if (
2717 + enableProfilerTimer &&
2718 + enableProfilerCommitHooks &&
2719 + fiber.mode & ProfileMode
2720 + ) {
2721 + startPassiveEffectTimer();
2722 + invokeGuardedCallback(
2723 + null,
2724 + invokePassiveEffectCreate,
2725 + null,
2726 + effect,
2727 + );
2728 + recordPassiveEffectDuration(fiber);
2729 + } else {
2730 + invokeGuardedCallback(
2731 + null,
2732 + invokePassiveEffectCreate,
2733 + null,
2734 + effect,
2735 + );
2736 + }
2737 + if (hasCaughtError()) {
2738 + invariant(fiber !== null, 'Should be working on an effect.');
2739 + const error = clearCaughtError();
2740 + captureCommitPhaseError(fiber, error);
2741 + }
2742 + resetCurrentDebugFiberInDEV();
2743 + } else {
2744 + try {
2745 + const create = effect.create;
2746 + if (
2747 + enableProfilerTimer &&
2748 + enableProfilerCommitHooks &&
2749 + fiber.mode & ProfileMode
2750 + ) {
2751 + try {
2752 + startPassiveEffectTimer();
2753 + effect.destroy = create();
2754 + } finally {
2755 + recordPassiveEffectDuration(fiber);
2756 + }
2757 + } else {
2758 + effect.destroy = create();
2759 + }
2760 + } catch (error) {
2761 + invariant(fiber !== null, 'Should be working on an effect.');
2762 + captureCommitPhaseError(fiber, error);
2763 + }
2764 + }
2765 + }
2766 +
2767 + effect = next;
2768 + } while (effect !== firstEffect);
2769 + }
2770 +}
2771 +
2772 +function flushPassiveUnmountEffects(firstChild: Fiber): void {
2773 + let fiber = firstChild;
2774 + while (fiber !== null) {
2775 + const deletions = fiber.deletions;
2776 + if (deletions !== null) {
2777 + for (let i = 0; i < deletions.length; i++) {
2778 + const fiberToDelete = deletions[i];
2779 + // If this fiber (or anything below it) has passive effects then traverse the subtree.
2780 + const primaryEffectTag = fiberToDelete.effectTag & PassiveMask;
2781 + const primarySubtreeTag = fiberToDelete.subtreeTag & PassiveSubtreeTag;
2782 + if (
2783 + primarySubtreeTag !== NoSubtreeTag ||
2784 + primaryEffectTag !== NoEffect
2785 + ) {
2786 + flushPassiveUnmountEffectsInsideOfDeletedTree(fiberToDelete);
2787 + }
2788 +
2789 + // Now that passive effects have been processed, it's safe to detach lingering pointers.
2790 + detachFiberAfterEffects(fiberToDelete);
2791 + }
2792 + }
2793 +
2794 + const child = fiber.child;
2795 + if (child !== null) {
2796 + // If any children have passive effects then traverse the subtree.
2797 + // Note that this requires checking subtreeTag of the current Fiber,
2798 + // rather than the subtreeTag/effectsTag of the first child,
2799 + // since that would not cover passive effects in siblings.
2800 + const primarySubtreeTag = fiber.subtreeTag & PassiveSubtreeTag;
2801 + if (primarySubtreeTag !== NoSubtreeTag) {
2802 + flushPassiveUnmountEffects(child);
2803 + }
2804 }
2805 +
2806 + switch (fiber.tag) {
2807 + case FunctionComponent:
2808 + case ForwardRef:
2809 + case SimpleMemoComponent:
2810 + case Block: {
2811 + const primaryEffectTag = fiber.effectTag & Passive;
2812 + if (primaryEffectTag !== NoEffect) {
2813 + flushPassiveUnmountEffectsImpl(fiber);
2814 + }
2815 + }
2816 + }
2817 +
2818 + fiber = fiber.sibling;
2819 }
2674 - if (!rootDoesHavePassiveEffects) {
2675 - rootDoesHavePassiveEffects = true;
2676 - scheduleCallback(NormalSchedulerPriority, () => {
2677 - flushPassiveEffects();
2678 - return null;
2679 - });
2820 +}
2821 +
2822 +function flushPassiveUnmountEffectsInsideOfDeletedTree(
2823 + firstChild: Fiber,
2824 +): void {
2825 + let fiber = firstChild;
2826 + while (fiber !== null) {
2827 + const child = fiber.child;
2828 + if (child !== null) {
2829 + // If any children have passive effects then traverse the subtree.
2830 + // Note that this requires checking subtreeTag of the current Fiber,
2831 + // rather than the subtreeTag/effectsTag of the first child,
2832 + // since that would not cover passive effects in siblings.
2833 + const primarySubtreeTag = fiber.subtreeTag & PassiveSubtreeTag;
2834 + if (primarySubtreeTag !== NoSubtreeTag) {
2835 + flushPassiveUnmountEffectsInsideOfDeletedTree(child);
2836 + }
2837 + }
2838 +
2839 + switch (fiber.tag) {
2840 + case FunctionComponent:
2841 + case ForwardRef:
2842 + case SimpleMemoComponent:
2843 + case Block: {
2844 + const primaryEffectTag = fiber.effectTag & Passive;
2845 + if (primaryEffectTag !== NoEffect) {
2846 + flushPassiveUnmountEffectsImpl(fiber);
2847 + }
2848 + }
2849 + }
2850 +
2851 + fiber = fiber.sibling;
2852 }
2853 }
2854
2683 -function invokePassiveEffectCreate(effect: HookEffect): void {
2684 - const create = effect.create;
2685 - effect.destroy = create();
2855 +function flushPassiveUnmountEffectsImpl(fiber: Fiber): void {
2856 + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any);
2857 + const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
2858 + if (lastEffect !== null) {
2859 + const firstEffect = lastEffect.next;
2860 + let effect = firstEffect;
2861 + do {
2862 + const {next, tag} = effect;
2863 + if (
2864 + (tag & HookPassive) !== NoHookEffect &&
2865 + (tag & HookHasEffect) !== NoHookEffect
2866 + ) {
2867 + const destroy = effect.destroy;
2868 + effect.destroy = undefined;
2869 +
2870 + if (typeof destroy === 'function') {
2871 + if (__DEV__) {
2872 + setCurrentDebugFiberInDEV(fiber);
2873 + if (
2874 + enableProfilerTimer &&
2875 + enableProfilerCommitHooks &&
2876 + fiber.mode & ProfileMode
2877 + ) {
2878 + startPassiveEffectTimer();
2879 + invokeGuardedCallback(null, destroy, null);
2880 + recordPassiveEffectDuration(fiber);
2881 + } else {
2882 + invokeGuardedCallback(null, destroy, null);
2883 + }
2884 + if (hasCaughtError()) {
2885 + invariant(fiber !== null, 'Should be working on an effect.');
2886 + const error = clearCaughtError();
2887 + captureCommitPhaseError(fiber, error);
2888 + }
2889 + resetCurrentDebugFiberInDEV();
2890 + } else {
2891 + try {
2892 + if (
2893 + enableProfilerTimer &&
2894 + enableProfilerCommitHooks &&
2895 + fiber.mode & ProfileMode
2896 + ) {
2897 + try {
2898 + startPassiveEffectTimer();
2899 + destroy();
2900 + } finally {
2901 + recordPassiveEffectDuration(fiber);
2902 + }
2903 + } else {
2904 + destroy();
2905 + }
2906 + } catch (error) {
2907 + invariant(fiber !== null, 'Should be working on an effect.');
2908 + captureCommitPhaseError(fiber, error);
2909 + }
2910 + }
2911 + }
2912 + }
2913 +
2914 + effect = next;
2915 + } while (effect !== firstEffect);
2916 + }
2917 }
2918
2919 function flushPassiveEffectsImpl() {
@@ -2724,117 +2955,8 @@ function flushPassiveEffectsImpl() {
2955 // e.g. a destroy function in one component may unintentionally override a ref
2956 // value set by a create function in another component.
2957 // Layout effects have the same constraint.
2727 -
2728 - // First pass: Destroy stale passive effects.
2729 - const unmountEffects = pendingPassiveHookEffectsUnmount;
2730 - pendingPassiveHookEffectsUnmount = [];
2731 - for (let i = 0; i < unmountEffects.length; i += 2) {
2732 - const effect = ((unmountEffects[i]: any): HookEffect);
2733 - const fiber = ((unmountEffects[i + 1]: any): Fiber);
2734 - const destroy = effect.destroy;
2735 - effect.destroy = undefined;
2736 -
2737 - if (__DEV__) {
2738 - fiber.effectTag &= ~PassiveUnmountPendingDev;
2739 - const alternate = fiber.alternate;
2740 - if (alternate !== null) {
2741 - alternate.effectTag &= ~PassiveUnmountPendingDev;
2742 - }
2743 - }
2744 -
2745 - if (typeof destroy === 'function') {
2746 - if (__DEV__) {
2747 - setCurrentDebugFiberInDEV(fiber);
2748 - if (
2749 - enableProfilerTimer &&
2750 - enableProfilerCommitHooks &&
2751 - fiber.mode & ProfileMode
2752 - ) {
2753 - startPassiveEffectTimer();
2754 - invokeGuardedCallback(null, destroy, null);
2755 - recordPassiveEffectDuration(fiber);
2756 - } else {
2757 - invokeGuardedCallback(null, destroy, null);
2758 - }
2759 - if (hasCaughtError()) {
2760 - invariant(fiber !== null, 'Should be working on an effect.');
2761 - const error = clearCaughtError();
2762 - captureCommitPhaseError(fiber, error);
2763 - }
2764 - resetCurrentDebugFiberInDEV();
2765 - } else {
2766 - try {
2767 - if (
2768 - enableProfilerTimer &&
2769 - enableProfilerCommitHooks &&
2770 - fiber.mode & ProfileMode
2771 - ) {
2772 - try {
2773 - startPassiveEffectTimer();
2774 - destroy();
2775 - } finally {
2776 - recordPassiveEffectDuration(fiber);
2777 - }
2778 - } else {
2779 - destroy();
2780 - }
2781 - } catch (error) {
2782 - invariant(fiber !== null, 'Should be working on an effect.');
2783 - captureCommitPhaseError(fiber, error);
2784 - }
2785 - }
2786 - }
2787 - }
2788 - // Second pass: Create new passive effects.
2789 - const mountEffects = pendingPassiveHookEffectsMount;
2790 - pendingPassiveHookEffectsMount = [];
2791 - for (let i = 0; i < mountEffects.length; i += 2) {
2792 - const effect = ((mountEffects[i]: any): HookEffect);
2793 - const fiber = ((mountEffects[i + 1]: any): Fiber);
2794 - if (__DEV__) {
2795 - setCurrentDebugFiberInDEV(fiber);
2796 - if (
2797 - enableProfilerTimer &&
2798 - enableProfilerCommitHooks &&
2799 - fiber.mode & ProfileMode
2800 - ) {
2801 - startPassiveEffectTimer();
2802 - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
2803 - recordPassiveEffectDuration(fiber);
2804 - } else {
2805 - invokeGuardedCallback(null, invokePassiveEffectCreate, null, effect);
2806 - }
2807 - if (hasCaughtError()) {
2808 - invariant(fiber !== null, 'Should be working on an effect.');
2809 - const error = clearCaughtError();
2810 - captureCommitPhaseError(fiber, error);
2811 - }
2812 - resetCurrentDebugFiberInDEV();
2813 - } else {
2814 - try {
2815 - const create = effect.create;
2816 - if (
2817 - enableProfilerTimer &&
2818 - enableProfilerCommitHooks &&
2819 - fiber.mode & ProfileMode
2820 - ) {
2821 - try {
2822 - startPassiveEffectTimer();
2823 - effect.destroy = create();
2824 - } finally {
2825 - recordPassiveEffectDuration(fiber);
2826 - }
2827 - } else {
2828 - effect.destroy = create();
2829 - }
2830 - } catch (error) {
2831 - invariant(fiber !== null, 'Should be working on an effect.');
2832 - captureCommitPhaseError(fiber, error);
2833 - }
2834 - }
2835 - }
2836 -
2837 - // TODO (effects) Detach sibling pointers for deleted Fibers
2958 + flushPassiveUnmountEffects(root.current);
2959 + flushPassiveMountEffects(root.current);
2960
2961 if (enableProfilerTimer && enableProfilerCommitHooks) {
2962 const profilerEffects = pendingPassiveProfilerEffects;
@@ -3230,10 +3352,24 @@ function warnAboutUpdateOnUnmountedFiberInDEV(fiber) {
3352 return;
3353 }
3354
3233 - // If there are pending passive effects unmounts for this Fiber,
3234 - // we can assume that they would have prevented this update.
3235 - if ((fiber.effectTag & PassiveUnmountPendingDev) !== NoEffect) {
3236 - return;
3355 + if ((fiber.effectTag & PassiveStatic) !== NoEffect) {
3356 + const updateQueue: FunctionComponentUpdateQueue | null = (fiber.updateQueue: any);
3357 + if (updateQueue !== null) {
3358 + const lastEffect = updateQueue.lastEffect;
3359 + if (lastEffect !== null) {
3360 + const firstEffect = lastEffect.next;
3361 +
3362 + let effect = firstEffect;
3363 + do {
3364 + if (effect.destroy !== undefined) {
3365 + if ((effect.tag & HookPassive) !== NoHookEffect) {
3366 + return;
3367 + }
3368 + }
3369 + effect = effect.next;
3370 + } while (effect !== firstEffect);
3371 + }
3372 + }
3373 }
3374
3375 // We show the whole stack but dedupe on the top component's name because
@@ -3934,3 +4070,23 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
4070 };
4071 }
4072 }
4073 +
4074 +function detachFiberAfterEffects(fiber: Fiber): void {
4075 + // Null out fields to improve GC for references that may be lingering (e.g. DevTools).
4076 + // Note that we already cleared the return pointer in detachFiberMutation().
4077 + fiber.child = null;
4078 + fiber.deletions = null;
4079 + fiber.dependencies = null;
4080 + fiber.firstEffect = null;
4081 + fiber.lastEffect = null;
4082 + fiber.memoizedProps = null;
4083 + fiber.memoizedState = null;
4084 + fiber.pendingProps = null;
4085 + fiber.sibling = null;
4086 + fiber.stateNode = null;
4087 + fiber.updateQueue = null;
4088 +
4089 + if (__DEV__) {
4090 + fiber._debugOwner = null;
4091 + }
4092 +}
packages/react-reconciler/src/ReactSideEffectTags.js
+37 -23
@@ -10,36 +10,50 @@
10 export type SideEffectTag = number;
11
12 // Don't change these two values. They're used by React Dev Tools.
13 -export const NoEffect = /* */ 0b000000000000000;
14 -export const PerformedWork = /* */ 0b000000000000001;
13 +export const NoEffect = /* */ 0b0000000000000000;
14 +export const PerformedWork = /* */ 0b0000000000000001;
15
16 // You can change the rest (and add more).
17 -export const Placement = /* */ 0b000000000000010;
18 -export const Update = /* */ 0b000000000000100;
19 -export const PlacementAndUpdate = /* */ 0b000000000000110;
20 -export const Deletion = /* */ 0b000000000001000;
21 -export const ContentReset = /* */ 0b000000000010000;
22 -export const Callback = /* */ 0b000000000100000;
23 -export const DidCapture = /* */ 0b000000001000000;
24 -export const Ref = /* */ 0b000000010000000;
25 -export const Snapshot = /* */ 0b000000100000000;
26 -export const Passive = /* */ 0b000001000000000;
27 -export const PassiveUnmountPendingDev = /* */ 0b010000000000000;
28 -export const Hydrating = /* */ 0b000010000000000;
29 -export const HydratingAndUpdate = /* */ 0b000010000000100;
17 +export const Placement = /* */ 0b0000000000000010;
18 +export const Update = /* */ 0b0000000000000100;
19 +export const PlacementAndUpdate = /* */ 0b0000000000000110;
20 +export const Deletion = /* */ 0b0000000000001000;
21 +export const ContentReset = /* */ 0b0000000000010000;
22 +export const Callback = /* */ 0b0000000000100000;
23 +export const DidCapture = /* */ 0b0000000001000000;
24 +export const Ref = /* */ 0b0000000010000000;
25 +export const Snapshot = /* */ 0b0000000100000000;
26 +export const Passive = /* */ 0b0000001000000000;
27 +// TODO (effects) Remove this bit once the new reconciler is synced to the old.
28 +export const PassiveUnmountPendingDev = /* */ 0b0010000000000000;
29 +export const Hydrating = /* */ 0b0000010000000000;
30 +export const HydratingAndUpdate = /* */ 0b0000010000000100;
31
32 // Passive & Update & Callback & Ref & Snapshot
32 -export const LifecycleEffectMask = /* */ 0b000001110100100;
33 +export const LifecycleEffectMask = /* */ 0b0000001110100100;
34
35 // Union of all host effects
35 -export const HostEffectMask = /* */ 0b000011111111111;
36 +export const HostEffectMask = /* */ 0b0000011111111111;
37
38 // These are not really side effects, but we still reuse this field.
38 -export const Incomplete = /* */ 0b000100000000000;
39 -export const ShouldCapture = /* */ 0b001000000000000;
40 -export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;
39 +export const Incomplete = /* */ 0b0000100000000000;
40 +export const ShouldCapture = /* */ 0b0001000000000000;
41 +export const ForceUpdateForLegacySuspense = /* */ 0b0100000000000000;
42 +
43 +// Static tags describe aspects of a fiber that are not specific to a render,
44 +// e.g. a fiber uses a passive effect (even if there are no updates on this particular render).
45 +// This enables us to defer more work in the unmount case,
46 +// since we can defer traversing the tree during layout to look for Passive effects,
47 +// and instead rely on the static flag as a signal that there may be cleanup work.
48 +export const PassiveStatic = /* */ 0b1000000000000000;
49
50 // Union of side effect groupings as pertains to subtreeTag
43 -export const BeforeMutationMask = /* */ 0b000001100001010;
44 -export const MutationMask = /* */ 0b000010010011110;
45 -export const LayoutMask = /* */ 0b000000010100100;
51 +export const BeforeMutationMask = /* */ 0b0000001100001010;
52 +export const MutationMask = /* */ 0b0000010010011110;
53 +export const LayoutMask = /* */ 0b0000000010100100;
54 +export const PassiveMask = /* */ 0b1000001000001000;
55 +
56 +// Union of tags that don't get reset on clones.
57 +// This allows certain concepts to persist without recalculting them,
58 +// e.g. whether a subtree contains passive effects or portals.
59 +export const StaticMask = /* */ 0b1000000000000000;
packages/react-reconciler/src/ReactSubtreeTags.js
+5 -4
@@ -9,7 +9,8 @@
9
10 export type SubtreeTag = number;
11
12 -export const NoEffect = /* */ 0b000;
13 -export const BeforeMutation = /* */ 0b001;
14 -export const Mutation = /* */ 0b010;
15 -export const Layout = /* */ 0b100;
12 +export const NoEffect = /* */ 0b0000;
13 +export const BeforeMutation = /* */ 0b0001;
14 +export const Mutation = /* */ 0b0010;
15 +export const Layout = /* */ 0b0100;
16 +export const Passive = /* */ 0b1000;
packages/react-reconciler/src/__tests__/ReactSuspense-test.internal.js
+2 -4
@@ -1454,7 +1454,7 @@ describe('ReactSuspense', () => {
1454 ]);
1455 });
1456
1457 - it('should call onInteractionScheduledWorkCompleted after suspending', done => {
1457 + it('should call onInteractionScheduledWorkCompleted after suspending', () => {
1458 const subscriber = {
1459 onInteractionScheduledWorkCompleted: jest.fn(),
1460 onInteractionTraced: jest.fn(),
@@ -1512,13 +1512,11 @@ describe('ReactSuspense', () => {
1512 jest.advanceTimersByTime(1000);
1513
1514 expect(Scheduler).toHaveYielded(['Promise resolved [C]']);
1515 - expect(Scheduler).toFlushExpired([
1515 + expect(Scheduler).toFlushAndYield([
1516 // Even though the promise for C was thrown three times, we should only
1517 // re-render once.
1518 'C',
1519 ]);
1520 -
1521 - done();
1520 });
1521
1522 expect(
packages/react-reconciler/src/__tests__/SchedulingProfiler-test.internal.js
+45 -18
@@ -486,24 +486,51 @@ describe('SchedulingProfiler', () => {
486 ReactTestRenderer.create(<Example />, {unstable_isConcurrent: true});
487 });
488
489 - expect(marks.map(normalizeCodeLocInfo)).toEqual([
490 - '--schedule-render-512',
491 - '--render-start-512',
492 - '--render-stop',
493 - '--commit-start-512',
494 - '--layout-effects-start-512',
495 - '--layout-effects-stop',
496 - '--commit-stop',
497 - '--passive-effects-start-512',
498 - toggleComponentStacks(
499 - '--schedule-state-update-1024-Example-\n in Example (at **)',
500 - ),
501 - '--passive-effects-stop',
502 - '--render-start-1024',
503 - '--render-stop',
504 - '--commit-start-1024',
505 - '--commit-stop',
506 - ]);
489 + gate(({old}) => {
490 + if (old) {
491 + expect(marks.map(normalizeCodeLocInfo)).toEqual([
492 + '--schedule-render-512',
493 + '--render-start-512',
494 + '--render-stop',
495 + '--commit-start-512',
496 + '--layout-effects-start-512',
497 + '--layout-effects-stop',
498 + '--commit-stop',
499 + '--passive-effects-start-512',
500 + toggleComponentStacks(
501 + '--schedule-state-update-1024-Example-\n in Example (at **)',
502 + ),
503 + '--passive-effects-stop',
504 + '--render-start-1024',
505 + '--render-stop',
506 + '--commit-start-1024',
507 + '--commit-stop',
508 + ]);
509 + } else {
510 + expect(marks.map(normalizeCodeLocInfo)).toEqual([
511 + '--schedule-render-512',
512 + '--render-start-512',
513 + '--render-stop',
514 + '--commit-start-512',
515 + '--layout-effects-start-512',
516 + '--layout-effects-stop',
517 + '--commit-stop',
518 + '--passive-effects-start-512',
519 + toggleComponentStacks(
520 + '--schedule-state-update-1024-Example-\n in Example (at **)',
521 + ),
522 + '--passive-effects-stop',
523 + '--render-start-1024',
524 + '--render-stop',
525 + '--commit-start-1024',
526 + '--layout-effects-start-1024',
527 + '--layout-effects-stop',
528 + '--commit-stop',
529 + '--passive-effects-start-1024',
530 + '--passive-effects-stop',
531 + ]);
532 + }
533 + });
534 });
535
536 // @gate enableSchedulingProfiler