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