Convert snapshot phase to depth-first traversal (#20622)
Andrew Clark committed
Jan 19, 2021 at 17:37 UTC
2a646f73e4f64000fcd1a483cec1c08614f20e9c
3 files changed
+167
-111
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+141
-29
@@ -75,9 +75,10 @@ import {
75
Hydrating,
76
HydratingAndUpdate,
77
Passive,
78
+ BeforeMutationMask,
79
MutationMask,
79
- PassiveMask,
80
LayoutMask,
81
+ PassiveMask,
82
PassiveUnmountPendingDev,
83
} from './ReactFiberFlags';
84
import getComponentName from 'shared/getComponentName';
@@ -128,6 +129,8 @@ import {
129
commitHydratedSuspenseInstance,
130
clearContainer,
131
prepareScopeUpdate,
132
+ prepareForCommit,
133
+ beforeActiveInstanceBlur,
134
} from './ReactFiberHostConfig';
135
import {
136
captureCommitPhaseError,
@@ -144,6 +147,7 @@ import {
147
Passive as HookPassive,
148
} from './ReactHookEffectTags';
149
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
150
+import {doesFiberContain} from './ReactFiberTreeReflection';
151
152
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
153
if (__DEV__) {
@@ -259,18 +263,114 @@ function safelyCallDestroy(current: Fiber, destroy: () => void) {
263
}
264
}
265
262
-function commitBeforeMutationLifeCycles(
263
- current: Fiber | null,
264
- finishedWork: Fiber,
265
-): void {
266
- switch (finishedWork.tag) {
267
- case FunctionComponent:
268
- case ForwardRef:
269
- case SimpleMemoComponent: {
266
+let focusedInstanceHandle: null | Fiber = null;
267
+let shouldFireAfterActiveInstanceBlur: boolean = false;
268
+
269
+export function commitBeforeMutationEffects(
270
+ root: FiberRoot,
271
+ firstChild: Fiber,
272
+) {
273
+ focusedInstanceHandle = prepareForCommit(root.containerInfo);
274
+
275
+ nextEffect = firstChild;
276
+ commitBeforeMutationEffects_begin();
277
+
278
+ // We no longer need to track the active instance fiber
279
+ const shouldFire = shouldFireAfterActiveInstanceBlur;
280
+ shouldFireAfterActiveInstanceBlur = false;
281
+ focusedInstanceHandle = null;
282
+
283
+ return shouldFire;
284
+}
285
+
286
+function commitBeforeMutationEffects_begin() {
287
+ while (nextEffect !== null) {
288
+ const fiber = nextEffect;
289
+
290
+ // TODO: Should wrap this in flags check, too, as optimization
291
+ const deletions = fiber.deletions;
292
+ if (deletions !== null) {
293
+ for (let i = 0; i < deletions.length; i++) {
294
+ const deletion = deletions[i];
295
+ commitBeforeMutationEffectsDeletion(deletion);
296
+ }
297
+ }
298
+
299
+ const child = fiber.child;
300
+ if (
301
+ (fiber.subtreeFlags & BeforeMutationMask) !== NoFlags &&
302
+ child !== null
303
+ ) {
304
+ ensureCorrectReturnPointer(child, fiber);
305
+ nextEffect = child;
306
+ } else {
307
+ commitBeforeMutationEffects_complete();
308
+ }
309
+ }
310
+}
311
+
312
+function commitBeforeMutationEffects_complete() {
313
+ while (nextEffect !== null) {
314
+ const fiber = nextEffect;
315
+ if (__DEV__) {
316
+ setCurrentDebugFiberInDEV(fiber);
317
+ invokeGuardedCallback(
318
+ null,
319
+ commitBeforeMutationEffectsOnFiber,
320
+ null,
321
+ fiber,
322
+ );
323
+ if (hasCaughtError()) {
324
+ const error = clearCaughtError();
325
+ captureCommitPhaseError(fiber, error);
326
+ }
327
+ resetCurrentDebugFiberInDEV();
328
+ } else {
329
+ try {
330
+ commitBeforeMutationEffectsOnFiber(fiber);
331
+ } catch (error) {
332
+ captureCommitPhaseError(fiber, error);
333
+ }
334
+ }
335
+
336
+ const sibling = fiber.sibling;
337
+ if (sibling !== null) {
338
+ ensureCorrectReturnPointer(sibling, fiber.return);
339
+ nextEffect = sibling;
340
return;
341
}
272
- case ClassComponent: {
273
- if (finishedWork.flags & Snapshot) {
342
+
343
+ nextEffect = fiber.return;
344
+ }
345
+}
346
+
347
+function commitBeforeMutationEffectsOnFiber(finishedWork: Fiber) {
348
+ const current = finishedWork.alternate;
349
+ const flags = finishedWork.flags;
350
+
351
+ if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
352
+ // Check to see if the focused element was inside of a hidden (Suspense) subtree.
353
+ // TODO: Move this out of the hot path using a dedicated effect tag.
354
+ if (
355
+ finishedWork.tag === SuspenseComponent &&
356
+ isSuspenseBoundaryBeingHidden(current, finishedWork) &&
357
+ doesFiberContain(finishedWork, focusedInstanceHandle)
358
+ ) {
359
+ shouldFireAfterActiveInstanceBlur = true;
360
+ beforeActiveInstanceBlur(finishedWork);
361
+ }
362
+ }
363
+
364
+ if ((flags & Snapshot) !== NoFlags) {
365
+ setCurrentDebugFiberInDEV(finishedWork);
366
+
367
+ switch (finishedWork.tag) {
368
+ case FunctionComponent:
369
+ case ForwardRef:
370
+ case SimpleMemoComponent: {
371
+ break;
372
+ }
373
+ case ClassComponent: {
374
if (current !== null) {
375
const prevProps = current.memoizedProps;
376
const prevState = current.memoizedState;
@@ -324,30 +424,43 @@ function commitBeforeMutationLifeCycles(
424
}
425
instance.__reactInternalSnapshotBeforeUpdate = snapshot;
426
}
427
+ break;
428
}
328
- return;
329
- }
330
- case HostRoot: {
331
- if (supportsMutation) {
332
- if (finishedWork.flags & Snapshot) {
429
+ case HostRoot: {
430
+ if (supportsMutation) {
431
const root = finishedWork.stateNode;
432
clearContainer(root.containerInfo);
433
}
434
+ break;
435
+ }
436
+ case HostComponent:
437
+ case HostText:
438
+ case HostPortal:
439
+ case IncompleteClassComponent:
440
+ // Nothing to do for these component types
441
+ break;
442
+ default: {
443
+ invariant(
444
+ false,
445
+ 'This unit of work tag should not have side-effects. This error is ' +
446
+ 'likely caused by a bug in React. Please file an issue.',
447
+ );
448
}
337
- return;
449
}
339
- case HostComponent:
340
- case HostText:
341
- case HostPortal:
342
- case IncompleteClassComponent:
343
- // Nothing to do for these component types
344
- return;
450
+
451
+ resetCurrentDebugFiberInDEV();
452
+ }
453
+}
454
+
455
+function commitBeforeMutationEffectsDeletion(deletion: Fiber) {
456
+ // TODO (effects) It would be nice to avoid calling doesFiberContain()
457
+ // Maybe we can repurpose one of the subtreeFlags positions for this instead?
458
+ // Use it to store which part of the tree the focused instance is in?
459
+ // This assumes we can safely determine that instance during the "render" phase.
460
+ if (doesFiberContain(deletion, ((focusedInstanceHandle: any): Fiber))) {
461
+ shouldFireAfterActiveInstanceBlur = true;
462
+ beforeActiveInstanceBlur(deletion);
463
}
346
- invariant(
347
- false,
348
- 'This unit of work tag should not have side-effects. This error is ' +
349
- 'likely caused by a bug in React. Please file an issue.',
350
- );
464
}
465
466
function commitHookEffectListUnmount(flags: HookFlags, finishedWork: Fiber) {
@@ -2353,7 +2466,6 @@ function ensureCorrectReturnPointer(fiber, expectedReturnFiber) {
2466
}
2467
2468
export {
2356
- commitBeforeMutationLifeCycles,
2469
commitResetTextContent,
2470
commitPlacement,
2471
commitDeletion,
packages/react-reconciler/src/ReactFiberFlags.js
+3
@@ -60,6 +60,9 @@ export const MountPassiveDev = /* */ 0b10000000000000000000;
60
// don't contain effects, by checking subtreeFlags.
61
62
export const BeforeMutationMask =
63
+ // TODO: Remove Update flag from before mutation phase by re-landing Visiblity
64
+ // flag logic (see #20043)
65
+ Update |
66
Snapshot |
67
(enableCreateEventHandleAPI
68
? // createEventHandle needs to visit deleted and hidden trees to
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+23
-82
@@ -83,13 +83,11 @@ import * as Scheduler from 'scheduler';
83
import {__interactionsRef, __subscriberRef} from 'scheduler/tracing';
84
85
import {
86
- prepareForCommit,
86
resetAfterCommit,
87
scheduleTimeout,
88
cancelTimeout,
89
noTimeout,
90
warnsIfNotActing,
92
- beforeActiveInstanceBlur,
91
afterActiveInstanceBlur,
92
clearContainer,
93
} from './ReactFiberHostConfig';
@@ -122,9 +120,7 @@ import {
120
NoFlags,
121
PerformedWork,
122
Placement,
125
- Deletion,
123
ChildDeletion,
127
- Snapshot,
124
Passive,
125
PassiveStatic,
126
Incomplete,
@@ -181,11 +177,10 @@ import {
177
createClassErrorUpdate,
178
} from './ReactFiberThrow.new';
179
import {
184
- commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
180
+ commitBeforeMutationEffects,
181
commitLayoutEffects,
182
commitMutationEffects,
183
commitPassiveEffectDurations,
188
- isSuspenseBoundaryBeingHidden,
184
commitPassiveMountEffects,
185
commitPassiveUnmountEffects,
186
detachFiberAfterEffects,
@@ -232,7 +227,6 @@ import {onCommitRoot as onCommitRootTestSelector} from './ReactTestSelectors';
227
228
// Used by `act`
229
import enqueueTask from 'shared/enqueueTask';
235
-import {doesFiberContain} from './ReactFiberTreeReflection';
230
231
const ceil = Math.ceil;
232
@@ -363,9 +357,6 @@ let currentEventPendingLanes: Lanes = NoLanes;
357
// We warn about state updates for unmounted components differently in this case.
358
let isFlushingPassiveEffects = false;
359
366
-let focusedInstanceHandle: null | Fiber = null;
367
-let shouldFireAfterActiveInstanceBlur: boolean = false;
368
-
360
export function getWorkInProgressRoot(): FiberRoot | null {
361
return workInProgressRoot;
362
}
@@ -1941,6 +1932,24 @@ function commitRootImpl(root, renderPriorityLevel) {
1932
firstEffect = finishedWork.firstEffect;
1933
}
1934
1935
+ // If there are pending passive effects, schedule a callback to process them.
1936
+ // Do this as early as possible, so it is queued before anything else that
1937
+ // might get scheduled in the commit phase. (See #16714.)
1938
+ // TODO: Delete all other places that schedule the passive effect callback
1939
+ // They're redundant.
1940
+ if (
1941
+ (finishedWork.subtreeFlags & Passive) !== NoFlags ||
1942
+ (finishedWork.flags & Passive) !== NoFlags
1943
+ ) {
1944
+ if (!rootDoesHavePassiveEffects) {
1945
+ rootDoesHavePassiveEffects = true;
1946
+ scheduleCallback(NormalSchedulerPriority, () => {
1947
+ flushPassiveEffects();
1948
+ return null;
1949
+ });
1950
+ }
1951
+ }
1952
+
1953
if (firstEffect !== null) {
1954
let previousLanePriority;
1955
if (decoupleUpdatePriorityFromScheduler) {
@@ -1962,32 +1971,10 @@ function commitRootImpl(root, renderPriorityLevel) {
1971
// The first phase a "before mutation" phase. We use this phase to read the
1972
// state of the host tree right before we mutate it. This is where
1973
// getSnapshotBeforeUpdate is called.
1965
- focusedInstanceHandle = prepareForCommit(root.containerInfo);
1966
- shouldFireAfterActiveInstanceBlur = false;
1967
-
1968
- nextEffect = firstEffect;
1969
- do {
1970
- if (__DEV__) {
1971
- invokeGuardedCallback(null, commitBeforeMutationEffects, null);
1972
- if (hasCaughtError()) {
1973
- invariant(nextEffect !== null, 'Should be working on an effect.');
1974
- const error = clearCaughtError();
1975
- captureCommitPhaseError(nextEffect, error);
1976
- nextEffect = nextEffect.nextEffect;
1977
- }
1978
- } else {
1979
- try {
1980
- commitBeforeMutationEffects();
1981
- } catch (error) {
1982
- invariant(nextEffect !== null, 'Should be working on an effect.');
1983
- captureCommitPhaseError(nextEffect, error);
1984
- nextEffect = nextEffect.nextEffect;
1985
- }
1986
- }
1987
- } while (nextEffect !== null);
1988
-
1989
- // We no longer need to track the active instance fiber
1990
- focusedInstanceHandle = null;
1974
+ const shouldFireAfterActiveInstanceBlur = commitBeforeMutationEffects(
1975
+ root,
1976
+ finishedWork,
1977
+ );
1978
1979
if (enableProfilerTimer) {
1980
// Mark the current commit time to be shared by all Profilers in this
@@ -2204,52 +2191,6 @@ function commitRootImpl(root, renderPriorityLevel) {
2191
return null;
2192
}
2193
2207
-function commitBeforeMutationEffects() {
2208
- while (nextEffect !== null) {
2209
- const current = nextEffect.alternate;
2210
-
2211
- if (!shouldFireAfterActiveInstanceBlur && focusedInstanceHandle !== null) {
2212
- if ((nextEffect.flags & Deletion) !== NoFlags) {
2213
- if (doesFiberContain(nextEffect, focusedInstanceHandle)) {
2214
- shouldFireAfterActiveInstanceBlur = true;
2215
- beforeActiveInstanceBlur(nextEffect);
2216
- }
2217
- } else {
2218
- // TODO: Move this out of the hot path using a dedicated effect tag.
2219
- if (
2220
- nextEffect.tag === SuspenseComponent &&
2221
- isSuspenseBoundaryBeingHidden(current, nextEffect) &&
2222
- doesFiberContain(nextEffect, focusedInstanceHandle)
2223
- ) {
2224
- shouldFireAfterActiveInstanceBlur = true;
2225
- beforeActiveInstanceBlur(nextEffect);
2226
- }
2227
- }
2228
- }
2229
-
2230
- const flags = nextEffect.flags;
2231
- if ((flags & Snapshot) !== NoFlags) {
2232
- setCurrentDebugFiberInDEV(nextEffect);
2233
-
2234
- commitBeforeMutationEffectOnFiber(current, nextEffect);
2235
-
2236
- resetCurrentDebugFiberInDEV();
2237
- }
2238
- if ((flags & Passive) !== NoFlags) {
2239
- // If there are passive effects, schedule a callback to flush at
2240
- // the earliest opportunity.
2241
- if (!rootDoesHavePassiveEffects) {
2242
- rootDoesHavePassiveEffects = true;
2243
- scheduleCallback(NormalSchedulerPriority, () => {
2244
- flushPassiveEffects();
2245
- return null;
2246
- });
2247
- }
2248
- }
2249
- nextEffect = nextEffect.nextEffect;
2250
- }
2251
-}
2252
-
2194
export function flushPassiveEffects(): boolean {
2195
// Returns whether passive effects were flushed.
2196
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {