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

Effects list refactor continued: did-bailout flag (#19322)

* Effects list rewrite * Improved deletions approach Process deletions as we traverse the tree during commit, before we process other effects. This has the result of better mimicking the previous sequencing. * Made deletions field nullable * Revert (no longer necessary) change to ReactNative test * Eagerly set Deletions effect on Fiber when adding child to deletions array * Initialize deletions array to null * Null out deletions array instead of splicing 🤡 * Removed TODO comment * Initial exploration on a did-bailout flag * fixed the rest of the bugs * Rolled temporary didBailout attribute into subtreeTag * addressed comments * Removed DidBailout subtree tag * Removed stale comment * use while loop instead of recursion for siblings * move bailout flag from while loop * Removed some unnecessary Deletion effectTags from children * Move Deletion effect assignment to deletions array initialization Co-authored-by: Luna <lunaris.ruan@gmail.com>

Brian Vaughn committed Jul 16, 2020 at 09:10 UTC a226b9b44591e2af85388fa88ddf76b3f3a9de6c
11 files changed +482 -266
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+8 -1
@@ -367,7 +367,14 @@ describe('ReactDOMServerPartialHydration', () => {
367 const span2 = container.getElementsByTagName('span')[0];
368 // This is a new node.
369 expect(span).not.toBe(span2);
370 - expect(ref.current).toBe(span2);
370 +
371 + if (gate(flags => flags.new)) {
372 + // The effects list refactor causes this to be null because the Suspense Offscreen's child
373 + // is null. However, since we can't hydrate Suspense in legacy this change in behavior is ok
374 + expect(ref.current).toBe(null);
375 + } else {
376 + expect(ref.current).toBe(span2);
377 + }
378
379 // Resolving the promise should render the final content.
380 suspend = false;
packages/react-reconciler/src/ReactChildFiber.new.js
+9 -1
@@ -280,6 +280,7 @@ function ChildReconciler(shouldTrackSideEffects) {
280 // deletions, so we can just append the deletion to the list. The remaining
281 // effects aren't added until the complete phase. Once we implement
282 // resuming, this may not be true.
283 + // TODO (effects) Get rid of effects list update here.
284 const last = returnFiber.lastEffect;
285 if (last !== null) {
286 last.nextEffect = childToDelete;
@@ -287,8 +288,15 @@ function ChildReconciler(shouldTrackSideEffects) {
288 } else {
289 returnFiber.firstEffect = returnFiber.lastEffect = childToDelete;
290 }
291 + const deletions = returnFiber.deletions;
292 + if (deletions === null) {
293 + returnFiber.deletions = [childToDelete];
294 + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
295 + returnFiber.effectTag |= Deletion;
296 + } else {
297 + deletions.push(childToDelete);
298 + }
299 childToDelete.nextEffect = null;
291 - childToDelete.effectTag = Deletion;
300 }
301
302 function deleteRemainingChildren(
packages/react-reconciler/src/ReactFiber.new.js
+7
@@ -30,6 +30,7 @@ import {
30 enableBlocksAPI,
31 } from 'shared/ReactFeatureFlags';
32 import {NoEffect, Placement} from './ReactSideEffectTags';
33 +import {NoEffect as NoSubtreeEffect} from './ReactSubtreeTags';
34 import {ConcurrentRoot, BlockingRoot} from './ReactRootTags';
35 import {
36 IndeterminateComponent,
@@ -144,6 +145,8 @@ function FiberNode(
145
146 // Effects
147 this.effectTag = NoEffect;
148 + this.subtreeTag = NoSubtreeEffect;
149 + this.deletions = null;
150 this.nextEffect = null;
151
152 this.firstEffect = null;
@@ -287,6 +290,8 @@ export function createWorkInProgress(current: Fiber, pendingProps: any): Fiber {
290 // We already have an alternate.
291 // Reset the effect tag.
292 workInProgress.effectTag = NoEffect;
293 + workInProgress.subtreeTag = NoSubtreeEffect;
294 + workInProgress.deletions = null;
295
296 // The effect list is no longer valid.
297 workInProgress.nextEffect = null;
@@ -826,6 +831,8 @@ export function assignFiberPropertiesInDEV(
831 target.dependencies = source.dependencies;
832 target.mode = source.mode;
833 target.effectTag = source.effectTag;
834 + target.subtreeTag = source.subtreeTag;
835 + target.deletions = source.deletions;
836 target.nextEffect = source.nextEffect;
837 target.firstEffect = source.firstEffect;
838 target.lastEffect = source.lastEffect;
packages/react-reconciler/src/ReactFiberBeginWork.new.js
+18 -1
@@ -2066,6 +2066,14 @@ function updateSuspensePrimaryChildren(
2066 currentFallbackChildFragment.nextEffect = null;
2067 currentFallbackChildFragment.effectTag = Deletion;
2068 workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChildFragment;
2069 + const deletions = workInProgress.deletions;
2070 + if (deletions === null) {
2071 + workInProgress.deletions = [currentFallbackChildFragment];
2072 + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
2073 + workInProgress.effectTag |= Deletion;
2074 + } else {
2075 + deletions.push(currentFallbackChildFragment);
2076 + }
2077 }
2078
2079 workInProgress.child = primaryChildFragment;
@@ -2131,9 +2139,11 @@ function updateSuspenseFallbackChildren(
2139 workInProgress.firstEffect = primaryChildFragment.firstEffect;
2140 workInProgress.lastEffect = progressedLastEffect;
2141 progressedLastEffect.nextEffect = null;
2142 + workInProgress.deletions = null;
2143 } else {
2144 // TODO: Reset this somewhere else? Lol legacy mode is so weird.
2145 workInProgress.firstEffect = workInProgress.lastEffect = null;
2146 + workInProgress.deletions = null;
2147 }
2148 } else {
2149 primaryChildFragment = createWorkInProgressOffscreenFiber(
@@ -3040,8 +3050,15 @@ function remountFiber(
3050 } else {
3051 returnFiber.firstEffect = returnFiber.lastEffect = current;
3052 }
3053 + const deletions = returnFiber.deletions;
3054 + if (deletions === null) {
3055 + returnFiber.deletions = [current];
3056 + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
3057 + returnFiber.effectTag |= Deletion;
3058 + } else {
3059 + deletions.push(current);
3060 + }
3061 current.nextEffect = null;
3044 - current.effectTag = Deletion;
3062
3063 newWorkInProgress.effectTag |= Placement;
3064
packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+6
@@ -1062,6 +1062,12 @@ function completeWork(
1062 // Reset the effect list before doing the second pass since that's now invalid.
1063 if (renderState.lastEffect === null) {
1064 workInProgress.firstEffect = null;
1065 + workInProgress.subtreeTag = NoEffect;
1066 + let child = workInProgress.child;
1067 + while (child !== null) {
1068 + child.deletions = null;
1069 + child = child.sibling;
1070 + }
1071 }
1072 workInProgress.lastEffect = renderState.lastEffect;
1073 // Reset the child fibers to their original state.
packages/react-reconciler/src/ReactFiberHydrationContext.new.js
+9 -1
@@ -24,7 +24,7 @@ import {
24 HostRoot,
25 SuspenseComponent,
26 } from './ReactWorkTags';
27 -import {Deletion, Placement, Hydrating} from './ReactSideEffectTags';
27 +import {Deletion, Hydrating, Placement} from './ReactSideEffectTags';
28 import invariant from 'shared/invariant';
29
30 import {
@@ -125,6 +125,14 @@ function deleteHydratableInstance(
125 childToDelete.stateNode = instance;
126 childToDelete.return = returnFiber;
127 childToDelete.effectTag = Deletion;
128 + const deletions = returnFiber.deletions;
129 + if (deletions === null) {
130 + returnFiber.deletions = [childToDelete];
131 + // TODO (effects) Rename this to better reflect its new usage (e.g. ChildDeletions)
132 + returnFiber.effectTag |= Deletion;
133 + } else {
134 + deletions.push(childToDelete);
135 + }
136
137 // This might seem like it belongs on progressedFirstDeletion. However,
138 // these children are not part of the reconciliation list of children.
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+402 -261
@@ -98,7 +98,6 @@ import {
98 Placement,
99 Update,
100 PlacementAndUpdate,
101 - Deletion,
101 Ref,
102 ContentReset,
103 Snapshot,
@@ -109,7 +108,16 @@ import {
108 HostEffectMask,
109 Hydrating,
110 HydratingAndUpdate,
111 + BeforeMutationMask,
112 + MutationMask,
113 + LayoutMask,
114 } from './ReactSideEffectTags';
115 +import {
116 + NoEffect as NoSubtreeTag,
117 + BeforeMutation,
118 + Mutation,
119 + Layout,
120 +} from './ReactSubtreeTags';
121 import {
122 NoLanePriority,
123 SyncLanePriority,
@@ -288,7 +296,6 @@ let globalMostRecentFallbackTime: number = 0;
296 const FALLBACK_THROTTLE_MS: number = 500;
297 const DEFAULT_TIMEOUT_MS: number = 5000;
298
291 -let nextEffect: Fiber | null = null;
299 let hasUncaughtError = false;
300 let firstUncaughtError = null;
301 let legacyErrorBoundariesThatAlreadyFailed: Set<mixed> | null = null;
@@ -1744,6 +1751,8 @@ function completeUnitOfWork(unitOfWork: Fiber): void {
1751 // Mark the parent fiber as incomplete and clear its effect list.
1752 returnFiber.firstEffect = returnFiber.lastEffect = null;
1753 returnFiber.effectTag |= Incomplete;
1754 + returnFiber.subtreeTag = NoSubtreeTag;
1755 + returnFiber.deletions = null;
1756 }
1757 }
1758
@@ -1780,60 +1789,133 @@ function resetChildLanes(completedWork: Fiber) {
1789 return;
1790 }
1791
1792 + const didBailout =
1793 + completedWork.alternate !== null &&
1794 + completedWork.alternate.child === completedWork.child;
1795 +
1796 let newChildLanes = NoLanes;
1797 + let subtreeTag = NoSubtreeTag;
1798 +
1799 + if (!didBailout) {
1800 + // Bubble up the earliest expiration time.
1801 + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
1802 + // In profiling mode, resetChildExpirationTime is also used to reset
1803 + // profiler durations.
1804 + let actualDuration = completedWork.actualDuration;
1805 + let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
1806 +
1807 + let child = completedWork.child;
1808 + while (child !== null) {
1809 + newChildLanes = mergeLanes(
1810 + newChildLanes,
1811 + mergeLanes(child.lanes, child.childLanes),
1812 + );
1813
1785 - // Bubble up the earliest expiration time.
1786 - if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
1787 - // In profiling mode, resetChildExpirationTime is also used to reset
1788 - // profiler durations.
1789 - let actualDuration = completedWork.actualDuration;
1790 - let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
1791 -
1792 - // When a fiber is cloned, its actualDuration is reset to 0. This value will
1793 - // only be updated if work is done on the fiber (i.e. it doesn't bailout).
1794 - // When work is done, it should bubble to the parent's actualDuration. If
1795 - // the fiber has not been cloned though, (meaning no work was done), then
1796 - // this value will reflect the amount of time spent working on a previous
1797 - // render. In that case it should not bubble. We determine whether it was
1798 - // cloned by comparing the child pointer.
1799 - const shouldBubbleActualDurations =
1800 - completedWork.alternate === null ||
1801 - completedWork.child !== completedWork.alternate.child;
1802 -
1803 - let child = completedWork.child;
1804 - while (child !== null) {
1805 - newChildLanes = mergeLanes(
1806 - newChildLanes,
1807 - mergeLanes(child.lanes, child.childLanes),
1808 - );
1809 - if (shouldBubbleActualDurations) {
1814 + subtreeTag |= child.subtreeTag;
1815 +
1816 + const effectTag = child.effectTag;
1817 + if ((effectTag & BeforeMutationMask) !== NoEffect) {
1818 + subtreeTag |= BeforeMutation;
1819 + }
1820 + if ((effectTag & MutationMask) !== NoEffect) {
1821 + subtreeTag |= Mutation;
1822 + }
1823 + if ((effectTag & LayoutMask) !== NoEffect) {
1824 + subtreeTag |= Layout;
1825 + }
1826 +
1827 + // When a fiber is cloned, its actualDuration is reset to 0. This value will
1828 + // only be updated if work is done on the fiber (i.e. it doesn't bailout).
1829 + // When work is done, it should bubble to the parent's actualDuration. If
1830 + // the fiber has not been cloned though, (meaning no work was done), then
1831 + // this value will reflect the amount of time spent working on a previous
1832 + // render. In that case it should not bubble. We determine whether it was
1833 + // cloned by comparing the child pointer.
1834 actualDuration += child.actualDuration;
1835 +
1836 + treeBaseDuration += child.treeBaseDuration;
1837 + child = child.sibling;
1838 + }
1839 +
1840 + const isTimedOutSuspense =
1841 + completedWork.tag === SuspenseComponent &&
1842 + completedWork.memoizedState !== null;
1843 + if (isTimedOutSuspense) {
1844 + // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1845 + const primaryChildFragment = completedWork.child;
1846 + if (primaryChildFragment !== null) {
1847 + treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1848 + }
1849 }
1812 - treeBaseDuration += child.treeBaseDuration;
1813 - child = child.sibling;
1814 - }
1850
1816 - const isTimedOutSuspense =
1817 - completedWork.tag === SuspenseComponent &&
1818 - completedWork.memoizedState !== null;
1819 - if (isTimedOutSuspense) {
1820 - // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1821 - const primaryChildFragment = completedWork.child;
1822 - if (primaryChildFragment !== null) {
1823 - treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1851 + completedWork.actualDuration = actualDuration;
1852 + completedWork.treeBaseDuration = treeBaseDuration;
1853 + } else {
1854 + let child = completedWork.child;
1855 + while (child !== null) {
1856 + newChildLanes = mergeLanes(
1857 + newChildLanes,
1858 + mergeLanes(child.lanes, child.childLanes),
1859 + );
1860 +
1861 + subtreeTag |= child.subtreeTag;
1862 +
1863 + const effectTag = child.effectTag;
1864 + if ((effectTag & BeforeMutationMask) !== NoEffect) {
1865 + subtreeTag |= BeforeMutation;
1866 + }
1867 + if ((effectTag & MutationMask) !== NoEffect) {
1868 + subtreeTag |= Mutation;
1869 + }
1870 + if ((effectTag & LayoutMask) !== NoEffect) {
1871 + subtreeTag |= Layout;
1872 + }
1873 +
1874 + child = child.sibling;
1875 }
1876 }
1877
1827 - completedWork.actualDuration = actualDuration;
1828 - completedWork.treeBaseDuration = treeBaseDuration;
1878 + completedWork.subtreeTag |= subtreeTag;
1879 } else {
1830 - let child = completedWork.child;
1831 - while (child !== null) {
1832 - newChildLanes = mergeLanes(
1833 - newChildLanes,
1834 - mergeLanes(child.lanes, child.childLanes),
1835 - );
1836 - child = child.sibling;
1880 + // Bubble up the earliest expiration time.
1881 + if (enableProfilerTimer && (completedWork.mode & ProfileMode) !== NoMode) {
1882 + // In profiling mode, resetChildExpirationTime is also used to reset
1883 + // profiler durations.
1884 + let treeBaseDuration = ((completedWork.selfBaseDuration: any): number);
1885 +
1886 + let child = completedWork.child;
1887 + while (child !== null) {
1888 + newChildLanes = mergeLanes(
1889 + newChildLanes,
1890 + mergeLanes(child.lanes, child.childLanes),
1891 + );
1892 +
1893 + treeBaseDuration += child.treeBaseDuration;
1894 + child = child.sibling;
1895 + }
1896 +
1897 + const isTimedOutSuspense =
1898 + completedWork.tag === SuspenseComponent &&
1899 + completedWork.memoizedState !== null;
1900 + if (isTimedOutSuspense) {
1901 + // Don't count time spent in a timed out Suspense subtree as part of the base duration.
1902 + const primaryChildFragment = completedWork.child;
1903 + if (primaryChildFragment !== null) {
1904 + treeBaseDuration -= ((primaryChildFragment.treeBaseDuration: any): number);
1905 + }
1906 + }
1907 +
1908 + completedWork.treeBaseDuration = treeBaseDuration;
1909 + } else {
1910 + let child = completedWork.child;
1911 + while (child !== null) {
1912 + newChildLanes = mergeLanes(
1913 + newChildLanes,
1914 + mergeLanes(child.lanes, child.childLanes),
1915 + );
1916 +
1917 + child = child.sibling;
1918 + }
1919 }
1920 }
1921
@@ -1952,26 +2034,7 @@ function commitRootImpl(root, renderPriorityLevel) {
2034 focusedInstanceHandle = prepareForCommit(root.containerInfo);
2035 shouldFireAfterActiveInstanceBlur = false;
2036
1955 - nextEffect = firstEffect;
1956 - do {
1957 - if (__DEV__) {
1958 - invokeGuardedCallback(null, commitBeforeMutationEffects, null);
1959 - if (hasCaughtError()) {
1960 - invariant(nextEffect !== null, 'Should be working on an effect.');
1961 - const error = clearCaughtError();
1962 - captureCommitPhaseError(nextEffect, error);
1963 - nextEffect = nextEffect.nextEffect;
1964 - }
1965 - } else {
1966 - try {
1967 - commitBeforeMutationEffects();
1968 - } catch (error) {
1969 - invariant(nextEffect !== null, 'Should be working on an effect.');
1970 - captureCommitPhaseError(nextEffect, error);
1971 - nextEffect = nextEffect.nextEffect;
1972 - }
1973 - }
1974 - } while (nextEffect !== null);
2037 + commitBeforeMutationEffects(finishedWork);
2038
2039 // We no longer need to track the active instance fiber
2040 focusedInstanceHandle = null;
@@ -1983,32 +2046,7 @@ function commitRootImpl(root, renderPriorityLevel) {
2046 }
2047
2048 // The next phase is the mutation phase, where we mutate the host tree.
1986 - nextEffect = firstEffect;
1987 - do {
1988 - if (__DEV__) {
1989 - invokeGuardedCallback(
1990 - null,
1991 - commitMutationEffects,
1992 - null,
1993 - root,
1994 - renderPriorityLevel,
1995 - );
1996 - if (hasCaughtError()) {
1997 - invariant(nextEffect !== null, 'Should be working on an effect.');
1998 - const error = clearCaughtError();
1999 - captureCommitPhaseError(nextEffect, error);
2000 - nextEffect = nextEffect.nextEffect;
2001 - }
2002 - } else {
2003 - try {
2004 - commitMutationEffects(root, renderPriorityLevel);
2005 - } catch (error) {
2006 - invariant(nextEffect !== null, 'Should be working on an effect.');
2007 - captureCommitPhaseError(nextEffect, error);
2008 - nextEffect = nextEffect.nextEffect;
2009 - }
2010 - }
2011 - } while (nextEffect !== null);
2049 + commitMutationEffects(finishedWork, root, renderPriorityLevel);
2050
2051 if (shouldFireAfterActiveInstanceBlur) {
2052 afterActiveInstanceBlur();
@@ -2024,28 +2062,7 @@ function commitRootImpl(root, renderPriorityLevel) {
2062 // The next phase is the layout phase, where we call effects that read
2063 // the host tree after it's been mutated. The idiomatic use case for this is
2064 // layout, but class component lifecycles also fire here for legacy reasons.
2027 - nextEffect = firstEffect;
2028 - do {
2029 - if (__DEV__) {
2030 - invokeGuardedCallback(null, commitLayoutEffects, null, root, lanes);
2031 - if (hasCaughtError()) {
2032 - invariant(nextEffect !== null, 'Should be working on an effect.');
2033 - const error = clearCaughtError();
2034 - captureCommitPhaseError(nextEffect, error);
2035 - nextEffect = nextEffect.nextEffect;
2036 - }
2037 - } else {
2038 - try {
2039 - commitLayoutEffects(root, lanes);
2040 - } catch (error) {
2041 - invariant(nextEffect !== null, 'Should be working on an effect.');
2042 - captureCommitPhaseError(nextEffect, error);
2043 - nextEffect = nextEffect.nextEffect;
2044 - }
2045 - }
2046 - } while (nextEffect !== null);
2047 -
2048 - nextEffect = null;
2065 + commitLayoutEffects(finishedWork, root, lanes);
2066
2067 // Tell Scheduler to yield at the end of the frame, so the browser has an
2068 // opportunity to paint.
@@ -2079,18 +2096,7 @@ function commitRootImpl(root, renderPriorityLevel) {
2096 pendingPassiveEffectsLanes = lanes;
2097 pendingPassiveEffectsRenderPriority = renderPriorityLevel;
2098 } else {
2082 - // We are done with the effect chain at this point so let's clear the
2083 - // nextEffect pointers to assist with GC. If we have passive effects, we'll
2084 - // clear this in flushPassiveEffects.
2085 - nextEffect = firstEffect;
2086 - while (nextEffect !== null) {
2087 - const nextNextEffect = nextEffect.nextEffect;
2088 - nextEffect.nextEffect = null;
2089 - if (nextEffect.effectTag & Deletion) {
2090 - detachFiberAfterEffects(nextEffect);
2091 - }
2092 - nextEffect = nextNextEffect;
2093 - }
2099 + // TODO (effects) Detach sibling pointers for deleted Fibers
2100 }
2101
2102 // Read this again, since an effect might have updated it
@@ -2172,160 +2178,311 @@ function commitRootImpl(root, renderPriorityLevel) {
2178 return null;
2179 }
2180
2175 -function commitBeforeMutationEffects() {
2176 - while (nextEffect !== null) {
2177 - const current = nextEffect.alternate;
2181 +function commitBeforeMutationEffects(firstChild: Fiber) {
2182 + let fiber = firstChild;
2183 + while (fiber !== null) {
2184 + if (fiber.deletions !== null) {
2185 + commitBeforeMutationEffectsDeletions(fiber.deletions);
2186 + }
2187
2179 - if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
2180 - if ((nextEffect.effectTag & Deletion) !== NoEffect) {
2181 - if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
2182 - shouldFireAfterActiveInstanceBlur = true;
2183 - beforeActiveInstanceBlur();
2184 - }
2185 - } else {
2186 - // TODO: Move this out of the hot path using a dedicated effect tag.
2187 - if (
2188 - nextEffect.tag === SuspenseComponent &&
2189 - isSuspenseBoundaryBeingHidden(current, nextEffect) &&
2190 - doesFiberContain(nextEffect, focusedInstanceHandle)
2191 - ) {
2192 - shouldFireAfterActiveInstanceBlur = true;
2193 - beforeActiveInstanceBlur();
2194 - }
2188 + if (fiber.child !== null) {
2189 + const primarySubtreeTag = fiber.subtreeTag & BeforeMutation;
2190 + if (primarySubtreeTag !== NoSubtreeTag) {
2191 + commitBeforeMutationEffects(fiber.child);
2192 }
2193 }
2194
2198 - const effectTag = nextEffect.effectTag;
2199 - if ((effectTag & Snapshot) !== NoEffect) {
2200 - setCurrentDebugFiberInDEV(nextEffect);
2195 + if (__DEV__) {
2196 + setCurrentDebugFiberInDEV(fiber);
2197 + invokeGuardedCallback(null, commitBeforeMutationEffectsImpl, null, fiber);
2198 + if (hasCaughtError()) {
2199 + const error = clearCaughtError();
2200 + captureCommitPhaseError(fiber, error);
2201 + }
2202 + resetCurrentDebugFiberInDEV();
2203 + } else {
2204 + try {
2205 + commitBeforeMutationEffectsImpl(fiber);
2206 + } catch (error) {
2207 + captureCommitPhaseError(fiber, error);
2208 + }
2209 + }
2210 + fiber = fiber.sibling;
2211 + }
2212 +}
2213
2202 - commitBeforeMutationEffectOnFiber(current, nextEffect);
2214 +function commitBeforeMutationEffectsImpl(fiber: Fiber) {
2215 + const current = fiber.alternate;
2216 + const effectTag = fiber.effectTag;
2217
2204 - resetCurrentDebugFiberInDEV();
2218 + if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
2219 + // Check to see if the focused element was inside of a hidden (Suspense) subtree.
2220 + // TODO: Move this out of the hot path using a dedicated effect tag.
2221 + if (
2222 + fiber.tag === SuspenseComponent &&
2223 + isSuspenseBoundaryBeingHidden(current, fiber) &&
2224 + doesFiberContain(fiber, focusedInstanceHandle)
2225 + ) {
2226 + shouldFireAfterActiveInstanceBlur = true;
2227 + beforeActiveInstanceBlur();
2228 }
2206 - if ((effectTag & Passive) !== NoEffect) {
2207 - // If there are passive effects, schedule a callback to flush at
2208 - // the earliest opportunity.
2209 - if (!rootDoesHavePassiveEffects) {
2210 - rootDoesHavePassiveEffects = true;
2211 - scheduleCallback(NormalSchedulerPriority, () => {
2212 - flushPassiveEffects();
2213 - return null;
2214 - });
2215 - }
2229 + }
2230 +
2231 + if ((effectTag & Snapshot) !== NoEffect) {
2232 + setCurrentDebugFiberInDEV(fiber);
2233 + commitBeforeMutationEffectOnFiber(current, fiber);
2234 + resetCurrentDebugFiberInDEV();
2235 + }
2236 +
2237 + if ((effectTag & Passive) !== NoEffect) {
2238 + // If there are passive effects, schedule a callback to flush at
2239 + // the earliest opportunity.
2240 + if (!rootDoesHavePassiveEffects) {
2241 + rootDoesHavePassiveEffects = true;
2242 + scheduleCallback(NormalSchedulerPriority, () => {
2243 + flushPassiveEffects();
2244 + return null;
2245 + });
2246 }
2217 - nextEffect = nextEffect.nextEffect;
2247 }
2248 }
2249
2221 -function commitMutationEffects(root: FiberRoot, renderPriorityLevel) {
2222 - // TODO: Should probably move the bulk of this function to commitWork.
2223 - while (nextEffect !== null) {
2224 - setCurrentDebugFiberInDEV(nextEffect);
2250 +function commitBeforeMutationEffectsDeletions(deletions: Array<Fiber>) {
2251 + for (let i = 0; i < deletions.length; i++) {
2252 + const fiber = deletions[i];
2253
2226 - const effectTag = nextEffect.effectTag;
2254 + // TODO (effects) It would be nice to avoid calling doesFiberContain()
2255 + // Maybe we can repurpose one of the subtreeTag positions for this instead?
2256 + // Use it to store which part of the tree the focused instance is in?
2257 + // This assumes we can safely determine that instance during the "render" phase.
2258
2228 - if (effectTag & ContentReset) {
2229 - commitResetTextContent(nextEffect);
2259 + if (doesFiberContain(fiber, ((focusedInstanceHandle: any): Fiber))) {
2260 + shouldFireAfterActiveInstanceBlur = true;
2261 + beforeActiveInstanceBlur();
2262 }
2263 + }
2264 +}
2265
2232 - if (effectTag & Ref) {
2233 - const current = nextEffect.alternate;
2234 - if (current !== null) {
2235 - commitDetachRef(current);
2236 - }
2237 - if (enableScopeAPI) {
2238 - // TODO: This is a temporary solution that allows us to transition away
2239 - // from React Flare on www.
2240 - if (nextEffect.tag === ScopeComponent) {
2241 - commitAttachRef(nextEffect);
2242 - }
2243 - }
2266 +function commitMutationEffects(
2267 + firstChild: Fiber,
2268 + root: FiberRoot,
2269 + renderPriorityLevel,
2270 +) {
2271 + let fiber = firstChild;
2272 + while (fiber !== null) {
2273 + if (fiber.deletions !== null) {
2274 + commitMutationEffectsDeletions(
2275 + fiber.deletions,
2276 + root,
2277 + renderPriorityLevel,
2278 + );
2279 +
2280 + // TODO (effects) Don't clear this yet; we may need to cleanup passive effects
2281 + fiber.deletions = null;
2282 }
2283
2246 - // The following switch statement is only concerned about placement,
2247 - // updates, and deletions. To avoid needing to add a case for every possible
2248 - // bitmap value, we remove the secondary effects from the effect tag and
2249 - // switch on that value.
2250 - const primaryEffectTag =
2251 - effectTag & (Placement | Update | Deletion | Hydrating);
2252 - switch (primaryEffectTag) {
2253 - case Placement: {
2254 - commitPlacement(nextEffect);
2255 - // Clear the "placement" from effect tag so that we know that this is
2256 - // inserted, before any life-cycles like componentDidMount gets called.
2257 - // TODO: findDOMNode doesn't rely on this any more but isMounted does
2258 - // and isMounted is deprecated anyway so we should be able to kill this.
2259 - nextEffect.effectTag &= ~Placement;
2260 - break;
2284 + if (fiber.child !== null) {
2285 + const primarySubtreeTag = fiber.subtreeTag & Mutation;
2286 + if (primarySubtreeTag !== NoSubtreeTag) {
2287 + commitMutationEffects(fiber.child, root, renderPriorityLevel);
2288 }
2262 - case PlacementAndUpdate: {
2263 - // Placement
2264 - commitPlacement(nextEffect);
2265 - // Clear the "placement" from effect tag so that we know that this is
2266 - // inserted, before any life-cycles like componentDidMount gets called.
2267 - nextEffect.effectTag &= ~Placement;
2289 + }
2290
2269 - // Update
2270 - const current = nextEffect.alternate;
2271 - commitWork(current, nextEffect);
2272 - break;
2291 + if (__DEV__) {
2292 + setCurrentDebugFiberInDEV(fiber);
2293 + invokeGuardedCallback(
2294 + null,
2295 + commitMutationEffectsImpl,
2296 + null,
2297 + fiber,
2298 + root,
2299 + renderPriorityLevel,
2300 + );
2301 + if (hasCaughtError()) {
2302 + const error = clearCaughtError();
2303 + captureCommitPhaseError(fiber, error);
2304 }
2274 - case Hydrating: {
2275 - nextEffect.effectTag &= ~Hydrating;
2276 - break;
2305 + resetCurrentDebugFiberInDEV();
2306 + } else {
2307 + try {
2308 + commitMutationEffectsImpl(fiber, root, renderPriorityLevel);
2309 + } catch (error) {
2310 + captureCommitPhaseError(fiber, error);
2311 }
2278 - case HydratingAndUpdate: {
2279 - nextEffect.effectTag &= ~Hydrating;
2312 + }
2313 + fiber = fiber.sibling;
2314 + }
2315 +}
2316
2281 - // Update
2282 - const current = nextEffect.alternate;
2283 - commitWork(current, nextEffect);
2284 - break;
2285 - }
2286 - case Update: {
2287 - const current = nextEffect.alternate;
2288 - commitWork(current, nextEffect);
2289 - break;
2290 - }
2291 - case Deletion: {
2292 - commitDeletion(root, nextEffect, renderPriorityLevel);
2293 - break;
2317 +function commitMutationEffectsImpl(
2318 + fiber: Fiber,
2319 + root: FiberRoot,
2320 + renderPriorityLevel,
2321 +) {
2322 + const effectTag = fiber.effectTag;
2323 + if (effectTag & ContentReset) {
2324 + commitResetTextContent(fiber);
2325 + }
2326 +
2327 + if (effectTag & Ref) {
2328 + const current = fiber.alternate;
2329 + if (current !== null) {
2330 + commitDetachRef(current);
2331 + }
2332 + if (enableScopeAPI) {
2333 + // TODO: This is a temporary solution that allows us to transition away
2334 + // from React Flare on www.
2335 + if (fiber.tag === ScopeComponent) {
2336 + commitAttachRef(fiber);
2337 }
2338 }
2339 + }
2340
2297 - resetCurrentDebugFiberInDEV();
2298 - nextEffect = nextEffect.nextEffect;
2341 + // The following switch statement is only concerned about placement,
2342 + // updates, and deletions. To avoid needing to add a case for every possible
2343 + // bitmap value, we remove the secondary effects from the effect tag and
2344 + // switch on that value.
2345 + const primaryEffectTag = effectTag & (Placement | Update | Hydrating);
2346 + switch (primaryEffectTag) {
2347 + case Placement: {
2348 + commitPlacement(fiber);
2349 + // Clear the "placement" from effect tag so that we know that this is
2350 + // inserted, before any life-cycles like componentDidMount gets called.
2351 + // TODO: findDOMNode doesn't rely on this any more but isMounted does
2352 + // and isMounted is deprecated anyway so we should be able to kill this.
2353 + fiber.effectTag &= ~Placement;
2354 + break;
2355 + }
2356 + case PlacementAndUpdate: {
2357 + // Placement
2358 + commitPlacement(fiber);
2359 + // Clear the "placement" from effect tag so that we know that this is
2360 + // inserted, before any life-cycles like componentDidMount gets called.
2361 + fiber.effectTag &= ~Placement;
2362 +
2363 + // Update
2364 + const current = fiber.alternate;
2365 + commitWork(current, fiber);
2366 + break;
2367 + }
2368 + case Hydrating: {
2369 + fiber.effectTag &= ~Hydrating;
2370 + break;
2371 + }
2372 + case HydratingAndUpdate: {
2373 + fiber.effectTag &= ~Hydrating;
2374 +
2375 + // Update
2376 + const current = fiber.alternate;
2377 + commitWork(current, fiber);
2378 + break;
2379 + }
2380 + case Update: {
2381 + const current = fiber.alternate;
2382 + commitWork(current, fiber);
2383 + break;
2384 + }
2385 }
2386 }
2387
2302 -function commitLayoutEffects(root: FiberRoot, committedLanes: Lanes) {
2303 - // TODO: Should probably move the bulk of this function to commitWork.
2304 - while (nextEffect !== null) {
2305 - setCurrentDebugFiberInDEV(nextEffect);
2306 -
2307 - const effectTag = nextEffect.effectTag;
2388 +function commitMutationEffectsDeletions(
2389 + deletions: Array<Fiber>,
2390 + root: FiberRoot,
2391 + renderPriorityLevel,
2392 +) {
2393 + for (let i = 0; i < deletions.length; i++) {
2394 + const childToDelete = deletions[i];
2395 + if (__DEV__) {
2396 + invokeGuardedCallback(
2397 + null,
2398 + commitDeletion,
2399 + null,
2400 + root,
2401 + childToDelete,
2402 + renderPriorityLevel,
2403 + );
2404 + if (hasCaughtError()) {
2405 + const error = clearCaughtError();
2406 + captureCommitPhaseError(childToDelete, error);
2407 + }
2408 + } else {
2409 + try {
2410 + commitDeletion(root, childToDelete, renderPriorityLevel);
2411 + } catch (error) {
2412 + captureCommitPhaseError(childToDelete, error);
2413 + }
2414 + }
2415 + // Don't clear the Deletion effect yet; we also use it to know when we need to detach refs later.
2416 + }
2417 +}
2418
2309 - if (effectTag & (Update | Callback)) {
2310 - const current = nextEffect.alternate;
2311 - commitLayoutEffectOnFiber(root, current, nextEffect, committedLanes);
2419 +function commitLayoutEffects(
2420 + firstChild: Fiber,
2421 + root: FiberRoot,
2422 + committedLanes: Lanes,
2423 +) {
2424 + let fiber = firstChild;
2425 + while (fiber !== null) {
2426 + if (fiber.child !== null) {
2427 + const primarySubtreeTag = fiber.subtreeTag & Layout;
2428 + if (primarySubtreeTag !== NoSubtreeTag) {
2429 + commitLayoutEffects(fiber.child, root, committedLanes);
2430 + }
2431 }
2432
2314 - if (enableScopeAPI) {
2315 - // TODO: This is a temporary solution that allows us to transition away
2316 - // from React Flare on www.
2317 - if (effectTag & Ref && nextEffect.tag !== ScopeComponent) {
2318 - commitAttachRef(nextEffect);
2433 + if (__DEV__) {
2434 + setCurrentDebugFiberInDEV(fiber);
2435 + invokeGuardedCallback(
2436 + null,
2437 + commitLayoutEffectsImpl,
2438 + null,
2439 + fiber,
2440 + root,
2441 + committedLanes,
2442 + );
2443 + if (hasCaughtError()) {
2444 + const error = clearCaughtError();
2445 + captureCommitPhaseError(fiber, error);
2446 }
2447 + resetCurrentDebugFiberInDEV();
2448 } else {
2321 - if (effectTag & Ref) {
2322 - commitAttachRef(nextEffect);
2449 + try {
2450 + commitLayoutEffectsImpl(fiber, root, committedLanes);
2451 + } catch (error) {
2452 + captureCommitPhaseError(fiber, error);
2453 }
2454 }
2455 + fiber = fiber.sibling;
2456 + }
2457 +}
2458
2326 - resetCurrentDebugFiberInDEV();
2327 - nextEffect = nextEffect.nextEffect;
2459 +function commitLayoutEffectsImpl(
2460 + fiber: Fiber,
2461 + root: FiberRoot,
2462 + committedLanes: Lanes,
2463 +) {
2464 + const effectTag = fiber.effectTag;
2465 +
2466 + setCurrentDebugFiberInDEV(fiber);
2467 +
2468 + if (effectTag & (Update | Callback)) {
2469 + const current = fiber.alternate;
2470 + commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
2471 + }
2472 +
2473 + if (enableScopeAPI) {
2474 + // TODO: This is a temporary solution that allows us to transition away
2475 + // from React Flare on www.
2476 + if (effectTag & Ref && fiber.tag !== ScopeComponent) {
2477 + commitAttachRef(fiber);
2478 + }
2479 + } else {
2480 + if (effectTag & Ref) {
2481 + commitAttachRef(fiber);
2482 + }
2483 }
2484 +
2485 + resetCurrentDebugFiberInDEV();
2486 }
2487
2488 export function flushPassiveEffects() {
@@ -2539,19 +2696,7 @@ function flushPassiveEffectsImpl() {
2696 }
2697 }
2698
2542 - // Note: This currently assumes there are no passive effects on the root fiber
2543 - // because the root is not part of its own effect list.
2544 - // This could change in the future.
2545 - let effect = root.current.firstEffect;
2546 - while (effect !== null) {
2547 - const nextNextEffect = effect.nextEffect;
2548 - // Remove nextEffect pointer to assist GC
2549 - effect.nextEffect = null;
2550 - if (effect.effectTag & Deletion) {
2551 - detachFiberAfterEffects(effect);
2552 - }
2553 - effect = nextNextEffect;
2554 - }
2699 + // TODO (effects) Detach sibling pointers for deleted Fibers
2700
2701 if (enableProfilerTimer && enableProfilerCommitHooks) {
2702 const profilerEffects = pendingPassiveProfilerEffects;
@@ -3638,7 +3783,3 @@ export function act(callback: () => Thenable<mixed>): Thenable<void> {
3783 };
3784 }
3785 }
3641 -
3642 -function detachFiberAfterEffects(fiber: Fiber): void {
3643 - fiber.sibling = null;
3644 -}
packages/react-reconciler/src/ReactInternalTypes.js
+3
@@ -23,6 +23,7 @@ import type {SuspenseInstance} from './ReactFiberHostConfig';
23 import type {WorkTag} from './ReactWorkTags';
24 import type {TypeOfMode} from './ReactTypeOfMode';
25 import type {SideEffectTag} from './ReactSideEffectTags';
26 +import type {SubtreeTag} from './ReactSubtreeTags';
27 import type {Lane, LanePriority, Lanes, LaneMap} from './ReactFiberLane';
28 import type {HookType} from './ReactFiberHooks.old';
29 import type {RootTag} from './ReactRootTags';
@@ -126,6 +127,8 @@ export type Fiber = {|
127
128 // Effect
129 effectTag: SideEffectTag,
130 + subtreeTag: SubtreeTag,
131 + deletions: Array<Fiber> | null,
132
133 // Singly linked list fast path to the next fiber with side-effects.
134 nextEffect: Fiber | null,
packages/react-reconciler/src/ReactSideEffectTags.js
+5
@@ -38,3 +38,8 @@ export const HostEffectMask = /* */ 0b000011111111111;
38 export const Incomplete = /* */ 0b000100000000000;
39 export const ShouldCapture = /* */ 0b001000000000000;
40 export const ForceUpdateForLegacySuspense = /* */ 0b100000000000000;
41 +
42 +// Union of side effect groupings as pertains to subtreeTag
43 +export const BeforeMutationMask = /* */ 0b000001100001010;
44 +export const MutationMask = /* */ 0b000010010011110;
45 +export const LayoutMask = /* */ 0b000000010100100;
packages/react-reconciler/src/ReactSubtreeTags.js new
+15
@@ -0,0 +1,15 @@
1 +/**
2 + * Copyright (c) Facebook, Inc. and its affiliates.
3 + *
4 + * This source code is licensed under the MIT license found in the
5 + * LICENSE file in the root directory of this source tree.
6 + *
7 + * @flow
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;
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
-1
@@ -1743,7 +1743,6 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1743 await resolveText('B2');
1744
1745 expect(Scheduler).toHaveYielded(['Promise resolved [B2]']);
1746 -
1746 expect(Scheduler).toFlushAndYield([
1747 'B2',
1748 'Destroy Layout Effect [Loading...]',