Refactor layout effect methods (#19895)
Commit phase durations (layout and passive) are stored on the nearest (ancestor) Profiler and bubble up during the commit phase. This bubbling used to be implemented by traversing the return path each time we finished working on a Profiler to find the next nearest Profiler. This commit removes that traversal. Instead, we maintain a stack of nearest Profiler ancestor while recursing the tree. This stack is maintained in the work loop (since that's where the recursive functions are) and so bubbling of durations has also been moved from commit-work to the work loop. This PR also refactors the methods used to recurse and apply effects in preparation for the new Offscreen component type.
Brian Vaughn committed
Sep 29, 2020 at 15:58 UTC
9198a5cec0936a21a5ba194a22fcbac03eba5d1d
2 files changed
+487
-372
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+433
-296
@@ -17,7 +17,6 @@ import type {
17
} from './ReactFiberHostConfig';
18
import type {Fiber} from './ReactInternalTypes';
19
import type {FiberRoot} from './ReactInternalTypes';
20
-import type {Lanes} from './ReactFiberLane';
20
import type {SuspenseState} from './ReactFiberSuspenseComponent.new';
21
import type {UpdateQueue} from './ReactUpdateQueue.new';
22
import type {FunctionComponentUpdateQueue} from './ReactFiberHooks.new';
@@ -70,11 +69,17 @@ import {
69
Snapshot,
70
Update,
71
Callback,
72
+ LayoutMask,
73
PassiveMask,
74
+ Ref,
75
} from './ReactFiberFlags';
76
import getComponentName from 'shared/getComponentName';
77
import invariant from 'shared/invariant';
77
-
78
+import {
79
+ current as currentDebugFiberInDEV,
80
+ resetCurrentFiber as resetCurrentDebugFiberInDEV,
81
+ setCurrentFiber as setCurrentDebugFiberInDEV,
82
+} from './ReactCurrentFiber';
83
import {onCommitUnmount} from './ReactFiberDevToolsHook.new';
84
import {resolveDefaultProps} from './ReactFiberLazyComponent.new';
85
import {
@@ -130,6 +135,9 @@ import {
135
} from './ReactHookEffectTags';
136
import {didWarnAboutReassigningProps} from './ReactFiberBeginWork.new';
137
138
+// Used to avoid traversing the return path to find the nearest Profiler ancestor during commit.
139
+let nearestProfilerOnStack: Fiber | null = null;
140
+
141
let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
142
if (__DEV__) {
143
didWarnAboutUndefinedSnapshotBeforeUpdate = new Set();
@@ -424,19 +432,6 @@ function commitProfilerPassiveEffect(
432
);
433
}
434
}
427
-
428
- // Bubble times to the next nearest ancestor Profiler.
429
- // After we process that Profiler, we'll bubble further up.
430
- // TODO: Use JS Stack instead
431
- let parentFiber = finishedWork.return;
432
- while (parentFiber !== null) {
433
- if (parentFiber.tag === Profiler) {
434
- const parentStateNode = parentFiber.stateNode;
435
- parentStateNode.passiveEffectDuration += passiveEffectDuration;
436
- break;
437
- }
438
- parentFiber = parentFiber.return;
439
- }
435
break;
436
}
437
default:
@@ -445,328 +440,470 @@ function commitProfilerPassiveEffect(
440
}
441
}
442
448
-function commitLifeCycles(
449
- finishedRoot: FiberRoot,
450
- current: Fiber | null,
443
+function recursivelyCommitLayoutEffects(
444
finishedWork: Fiber,
452
- committedLanes: Lanes,
453
-): void {
454
- switch (finishedWork.tag) {
455
- case FunctionComponent:
456
- case ForwardRef:
457
- case SimpleMemoComponent:
458
- case Block: {
459
- // At this point layout effects have already been destroyed (during mutation phase).
460
- // This is done to prevent sibling component effects from interfering with each other,
461
- // e.g. a destroy function in one component should never override a ref set
462
- // by a create function in another component during the same commit.
463
- if (
464
- enableProfilerTimer &&
465
- enableProfilerCommitHooks &&
466
- finishedWork.mode & ProfileMode
467
- ) {
468
- try {
469
- startLayoutEffectTimer();
470
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
471
- } finally {
472
- recordLayoutEffectDuration(finishedWork);
473
- }
474
- } else {
475
- commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
445
+ finishedRoot: FiberRoot,
446
+) {
447
+ const {flags, tag} = finishedWork;
448
+ switch (tag) {
449
+ case Profiler: {
450
+ let prevProfilerOnStack = null;
451
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
452
+ prevProfilerOnStack = nearestProfilerOnStack;
453
+ nearestProfilerOnStack = finishedWork;
454
}
455
478
- if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) {
479
- schedulePassiveEffectCallback();
480
- }
481
- return;
482
- }
483
- case ClassComponent: {
484
- const instance = finishedWork.stateNode;
485
- if (finishedWork.flags & Update) {
486
- if (current === null) {
487
- // We could update instance props and state here,
488
- // but instead we rely on them being set during last render.
489
- // TODO: revisit this when we implement resuming.
456
+ let child = finishedWork.child;
457
+ while (child !== null) {
458
+ const primarySubtreeFlags = finishedWork.subtreeFlags & LayoutMask;
459
+ if (primarySubtreeFlags !== NoFlags) {
460
if (__DEV__) {
491
- if (
492
- finishedWork.type === finishedWork.elementType &&
493
- !didWarnAboutReassigningProps
494
- ) {
495
- if (instance.props !== finishedWork.memoizedProps) {
496
- console.error(
497
- 'Expected %s props to match memoized props before ' +
498
- 'componentDidMount. ' +
499
- 'This might either be because of a bug in React, or because ' +
500
- 'a component reassigns its own `this.props`. ' +
501
- 'Please file an issue.',
502
- getComponentName(finishedWork.type) || 'instance',
503
- );
504
- }
505
- if (instance.state !== finishedWork.memoizedState) {
506
- console.error(
507
- 'Expected %s state to match memoized state before ' +
508
- 'componentDidMount. ' +
509
- 'This might either be because of a bug in React, or because ' +
510
- 'a component reassigns its own `this.state`. ' +
511
- 'Please file an issue.',
512
- getComponentName(finishedWork.type) || 'instance',
513
- );
514
- }
461
+ const prevCurrentFiberInDEV = currentDebugFiberInDEV;
462
+ setCurrentDebugFiberInDEV(child);
463
+ invokeGuardedCallback(
464
+ null,
465
+ recursivelyCommitLayoutEffects,
466
+ null,
467
+ child,
468
+ finishedRoot,
469
+ );
470
+ if (hasCaughtError()) {
471
+ const error = clearCaughtError();
472
+ captureCommitPhaseError(child, finishedWork, error);
473
}
516
- }
517
- if (
518
- enableProfilerTimer &&
519
- enableProfilerCommitHooks &&
520
- finishedWork.mode & ProfileMode
521
- ) {
522
- try {
523
- startLayoutEffectTimer();
524
- instance.componentDidMount();
525
- } finally {
526
- recordLayoutEffectDuration(finishedWork);
474
+ if (prevCurrentFiberInDEV !== null) {
475
+ setCurrentDebugFiberInDEV(prevCurrentFiberInDEV);
476
+ } else {
477
+ resetCurrentDebugFiberInDEV();
478
}
479
} else {
529
- instance.componentDidMount();
480
+ try {
481
+ recursivelyCommitLayoutEffects(child, finishedRoot);
482
+ } catch (error) {
483
+ captureCommitPhaseError(child, finishedWork, error);
484
+ }
485
}
531
- } else {
532
- const prevProps =
533
- finishedWork.elementType === finishedWork.type
534
- ? current.memoizedProps
535
- : resolveDefaultProps(finishedWork.type, current.memoizedProps);
536
- const prevState = current.memoizedState;
537
- // We could update instance props and state here,
538
- // but instead we rely on them being set during last render.
539
- // TODO: revisit this when we implement resuming.
486
+ }
487
+ child = child.sibling;
488
+ }
489
+
490
+ const primaryFlags = flags & (Update | Callback);
491
+ if (primaryFlags !== NoFlags) {
492
+ if (enableProfilerTimer) {
493
if (__DEV__) {
541
- if (
542
- finishedWork.type === finishedWork.elementType &&
543
- !didWarnAboutReassigningProps
544
- ) {
545
- if (instance.props !== finishedWork.memoizedProps) {
546
- console.error(
547
- 'Expected %s props to match memoized props before ' +
548
- 'componentDidUpdate. ' +
549
- 'This might either be because of a bug in React, or because ' +
550
- 'a component reassigns its own `this.props`. ' +
551
- 'Please file an issue.',
552
- getComponentName(finishedWork.type) || 'instance',
553
- );
554
- }
555
- if (instance.state !== finishedWork.memoizedState) {
556
- console.error(
557
- 'Expected %s state to match memoized state before ' +
558
- 'componentDidUpdate. ' +
559
- 'This might either be because of a bug in React, or because ' +
560
- 'a component reassigns its own `this.state`. ' +
561
- 'Please file an issue.',
562
- getComponentName(finishedWork.type) || 'instance',
563
- );
564
- }
494
+ const prevCurrentFiberInDEV = currentDebugFiberInDEV;
495
+ setCurrentDebugFiberInDEV(finishedWork);
496
+ invokeGuardedCallback(
497
+ null,
498
+ commitLayoutEffectsForProfiler,
499
+ null,
500
+ finishedWork,
501
+ finishedRoot,
502
+ );
503
+ if (hasCaughtError()) {
504
+ const error = clearCaughtError();
505
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
506
}
566
- }
567
- if (
568
- enableProfilerTimer &&
569
- enableProfilerCommitHooks &&
570
- finishedWork.mode & ProfileMode
571
- ) {
572
- try {
573
- startLayoutEffectTimer();
574
- instance.componentDidUpdate(
575
- prevProps,
576
- prevState,
577
- instance.__reactInternalSnapshotBeforeUpdate,
578
- );
579
- } finally {
580
- recordLayoutEffectDuration(finishedWork);
507
+ if (prevCurrentFiberInDEV !== null) {
508
+ setCurrentDebugFiberInDEV(prevCurrentFiberInDEV);
509
+ } else {
510
+ resetCurrentDebugFiberInDEV();
511
}
512
} else {
583
- instance.componentDidUpdate(
584
- prevProps,
585
- prevState,
586
- instance.__reactInternalSnapshotBeforeUpdate,
513
+ try {
514
+ commitLayoutEffectsForProfiler(finishedWork, finishedRoot);
515
+ } catch (error) {
516
+ captureCommitPhaseError(finishedWork, finishedWork.return, error);
517
+ }
518
+ }
519
+ }
520
+ }
521
+
522
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
523
+ // Propagate layout effect durations to the next nearest Profiler ancestor.
524
+ // Do not reset these values until the next render so DevTools has a chance to read them first.
525
+ if (prevProfilerOnStack !== null) {
526
+ prevProfilerOnStack.stateNode.effectDuration +=
527
+ finishedWork.stateNode.effectDuration;
528
+ }
529
+
530
+ nearestProfilerOnStack = prevProfilerOnStack;
531
+ }
532
+ break;
533
+ }
534
+
535
+ // case Offscreen: {
536
+ // TODO: Fast path to invoke all nested layout effects when Offscren goes from hidden to visible.
537
+ // break;
538
+ // }
539
+
540
+ default: {
541
+ let child = finishedWork.child;
542
+ while (child !== null) {
543
+ const primarySubtreeFlags = finishedWork.subtreeFlags & LayoutMask;
544
+ if (primarySubtreeFlags !== NoFlags) {
545
+ if (__DEV__) {
546
+ const prevCurrentFiberInDEV = currentDebugFiberInDEV;
547
+ setCurrentDebugFiberInDEV(child);
548
+ invokeGuardedCallback(
549
+ null,
550
+ recursivelyCommitLayoutEffects,
551
+ null,
552
+ child,
553
+ finishedRoot,
554
);
555
+ if (hasCaughtError()) {
556
+ const error = clearCaughtError();
557
+ captureCommitPhaseError(child, finishedWork, error);
558
+ }
559
+ if (prevCurrentFiberInDEV !== null) {
560
+ setCurrentDebugFiberInDEV(prevCurrentFiberInDEV);
561
+ } else {
562
+ resetCurrentDebugFiberInDEV();
563
+ }
564
+ } else {
565
+ try {
566
+ recursivelyCommitLayoutEffects(child, finishedRoot);
567
+ } catch (error) {
568
+ captureCommitPhaseError(child, finishedWork, error);
569
+ }
570
}
571
}
572
+ child = child.sibling;
573
}
574
592
- // TODO: I think this is now always non-null by the time it reaches the
593
- // commit phase. Consider removing the type check.
594
- const updateQueue: UpdateQueue<
595
- *,
596
- > | null = (finishedWork.updateQueue: any);
597
- if (updateQueue !== null) {
598
- if (__DEV__) {
599
- if (
600
- finishedWork.type === finishedWork.elementType &&
601
- !didWarnAboutReassigningProps
602
- ) {
603
- if (instance.props !== finishedWork.memoizedProps) {
604
- console.error(
605
- 'Expected %s props to match memoized props before ' +
606
- 'processing the update queue. ' +
607
- 'This might either be because of a bug in React, or because ' +
608
- 'a component reassigns its own `this.props`. ' +
609
- 'Please file an issue.',
610
- getComponentName(finishedWork.type) || 'instance',
575
+ const primaryFlags = flags & (Update | Callback);
576
+ if (primaryFlags !== NoFlags) {
577
+ switch (tag) {
578
+ case FunctionComponent:
579
+ case ForwardRef:
580
+ case SimpleMemoComponent:
581
+ case Block: {
582
+ if (
583
+ enableProfilerTimer &&
584
+ enableProfilerCommitHooks &&
585
+ finishedWork.mode & ProfileMode
586
+ ) {
587
+ try {
588
+ startLayoutEffectTimer();
589
+ commitHookEffectListMount(
590
+ HookLayout | HookHasEffect,
591
+ finishedWork,
592
+ );
593
+ } finally {
594
+ recordLayoutEffectDuration(finishedWork);
595
+ }
596
+ } else {
597
+ commitHookEffectListMount(
598
+ HookLayout | HookHasEffect,
599
+ finishedWork,
600
);
601
}
613
- if (instance.state !== finishedWork.memoizedState) {
614
- console.error(
615
- 'Expected %s state to match memoized state before ' +
616
- 'processing the update queue. ' +
617
- 'This might either be because of a bug in React, or because ' +
618
- 'a component reassigns its own `this.state`. ' +
619
- 'Please file an issue.',
620
- getComponentName(finishedWork.type) || 'instance',
621
- );
602
+
603
+ if ((finishedWork.subtreeFlags & PassiveMask) !== NoFlags) {
604
+ schedulePassiveEffectCallback();
605
}
606
+ break;
607
+ }
608
+ case ClassComponent: {
609
+ // NOTE: Layout effect durations are measured within this function.
610
+ commitLayoutEffectsForClassComponent(finishedWork);
611
+ break;
612
+ }
613
+ case HostRoot: {
614
+ commitLayoutEffectsForHostRoot(finishedWork);
615
+ break;
616
+ }
617
+ case HostComponent: {
618
+ commitLayoutEffectsForHostComponent(finishedWork);
619
+ break;
620
+ }
621
+ case SuspenseComponent: {
622
+ commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
623
+ break;
624
+ }
625
+ case FundamentalComponent:
626
+ case HostPortal:
627
+ case HostText:
628
+ case IncompleteClassComponent:
629
+ case LegacyHiddenComponent:
630
+ case OffscreenComponent:
631
+ case ScopeComponent:
632
+ case SuspenseListComponent: {
633
+ // We have no life-cycles associated with these component types.
634
+ break;
635
+ }
636
+ default: {
637
+ invariant(
638
+ false,
639
+ 'This unit of work tag should not have side-effects. This error is ' +
640
+ 'likely caused by a bug in React. Please file an issue.',
641
+ );
642
}
643
}
625
- // We could update instance props and state here,
626
- // but instead we rely on them being set during last render.
627
- // TODO: revisit this when we implement resuming.
628
- commitUpdateQueue(finishedWork, updateQueue, instance);
644
}
630
- return;
631
- }
632
- case HostRoot: {
633
- // TODO: I think this is now always non-null by the time it reaches the
634
- // commit phase. Consider removing the type check.
635
- const updateQueue: UpdateQueue<
636
- *,
637
- > | null = (finishedWork.updateQueue: any);
638
- if (updateQueue !== null) {
639
- let instance = null;
640
- if (finishedWork.child !== null) {
641
- switch (finishedWork.child.tag) {
642
- case HostComponent:
643
- instance = getPublicInstance(finishedWork.child.stateNode);
644
- break;
645
- case ClassComponent:
646
- instance = finishedWork.child.stateNode;
647
- break;
648
- }
645
+
646
+ if (enableScopeAPI) {
647
+ // TODO: This is a temporary solution that allowed us to transition away from React Flare on www.
648
+ if (flags & Ref && tag !== ScopeComponent) {
649
+ commitAttachRef(finishedWork);
650
+ }
651
+ } else {
652
+ if (flags & Ref) {
653
+ commitAttachRef(finishedWork);
654
}
650
- commitUpdateQueue(finishedWork, updateQueue, instance);
655
}
652
- return;
656
+ break;
657
}
654
- case HostComponent: {
655
- const instance: Instance = finishedWork.stateNode;
658
+ }
659
+}
660
657
- // Renderers may schedule work to be done after host components are mounted
658
- // (eg DOM renderer may schedule auto-focus for inputs and form controls).
659
- // These effects should only be committed when components are first mounted,
660
- // aka when there is no current/alternate.
661
- if (current === null && finishedWork.flags & Update) {
662
- const type = finishedWork.type;
663
- const props = finishedWork.memoizedProps;
664
- commitMount(instance, type, props, finishedWork);
665
- }
661
+function commitLayoutEffectsForProfiler(
662
+ finishedWork: Fiber,
663
+ finishedRoot: FiberRoot,
664
+) {
665
+ if (enableProfilerTimer) {
666
+ const flags = finishedWork.flags;
667
+ const current = finishedWork.alternate;
668
667
- return;
668
- }
669
- case HostText: {
670
- // We have no life-cycles associated with text.
671
- return;
672
- }
673
- case HostPortal: {
674
- // We have no life-cycles associated with portals.
675
- return;
676
- }
677
- case Profiler: {
678
- if (enableProfilerTimer) {
679
- const {onCommit, onRender} = finishedWork.memoizedProps;
680
- const {effectDuration} = finishedWork.stateNode;
681
- const flags = finishedWork.flags;
669
+ const {onCommit, onRender} = finishedWork.memoizedProps;
670
+ const {effectDuration} = finishedWork.stateNode;
671
683
- const commitTime = getCommitTime();
672
+ const commitTime = getCommitTime();
673
+
674
+ const OnRenderFlag = Update;
675
+ const OnCommitFlag = Callback;
676
685
- const OnRenderFlag = Update;
686
- const OnCommitFlag = Callback;
677
+ if ((flags & OnRenderFlag) !== NoFlags && typeof onRender === 'function') {
678
+ if (enableSchedulerTracing) {
679
+ onRender(
680
+ finishedWork.memoizedProps.id,
681
+ current === null ? 'mount' : 'update',
682
+ finishedWork.actualDuration,
683
+ finishedWork.treeBaseDuration,
684
+ finishedWork.actualStartTime,
685
+ commitTime,
686
+ finishedRoot.memoizedInteractions,
687
+ );
688
+ } else {
689
+ onRender(
690
+ finishedWork.memoizedProps.id,
691
+ current === null ? 'mount' : 'update',
692
+ finishedWork.actualDuration,
693
+ finishedWork.treeBaseDuration,
694
+ finishedWork.actualStartTime,
695
+ commitTime,
696
+ );
697
+ }
698
+ }
699
700
+ if (enableProfilerCommitHooks) {
701
+ if (
702
+ (flags & OnCommitFlag) !== NoFlags &&
703
+ typeof onCommit === 'function'
704
+ ) {
705
+ if (enableSchedulerTracing) {
706
+ onCommit(
707
+ finishedWork.memoizedProps.id,
708
+ current === null ? 'mount' : 'update',
709
+ effectDuration,
710
+ commitTime,
711
+ finishedRoot.memoizedInteractions,
712
+ );
713
+ } else {
714
+ onCommit(
715
+ finishedWork.memoizedProps.id,
716
+ current === null ? 'mount' : 'update',
717
+ effectDuration,
718
+ commitTime,
719
+ );
720
+ }
721
+ }
722
+ }
723
+ }
724
+}
725
+
726
+function commitLayoutEffectsForClassComponent(finishedWork: Fiber) {
727
+ const instance = finishedWork.stateNode;
728
+ const current = finishedWork.alternate;
729
+ if (finishedWork.flags & Update) {
730
+ if (current === null) {
731
+ // We could update instance props and state here,
732
+ // but instead we rely on them being set during last render.
733
+ // TODO: revisit this when we implement resuming.
734
+ if (__DEV__) {
735
if (
689
- (flags & OnRenderFlag) !== NoFlags &&
690
- typeof onRender === 'function'
736
+ finishedWork.type === finishedWork.elementType &&
737
+ !didWarnAboutReassigningProps
738
) {
692
- if (enableSchedulerTracing) {
693
- onRender(
694
- finishedWork.memoizedProps.id,
695
- current === null ? 'mount' : 'update',
696
- finishedWork.actualDuration,
697
- finishedWork.treeBaseDuration,
698
- finishedWork.actualStartTime,
699
- commitTime,
700
- finishedRoot.memoizedInteractions,
739
+ if (instance.props !== finishedWork.memoizedProps) {
740
+ console.error(
741
+ 'Expected %s props to match memoized props before ' +
742
+ 'componentDidMount. ' +
743
+ 'This might either be because of a bug in React, or because ' +
744
+ 'a component reassigns its own `this.props`. ' +
745
+ 'Please file an issue.',
746
+ getComponentName(finishedWork.type) || 'instance',
747
);
702
- } else {
703
- onRender(
704
- finishedWork.memoizedProps.id,
705
- current === null ? 'mount' : 'update',
706
- finishedWork.actualDuration,
707
- finishedWork.treeBaseDuration,
708
- finishedWork.actualStartTime,
709
- commitTime,
748
+ }
749
+ if (instance.state !== finishedWork.memoizedState) {
750
+ console.error(
751
+ 'Expected %s state to match memoized state before ' +
752
+ 'componentDidMount. ' +
753
+ 'This might either be because of a bug in React, or because ' +
754
+ 'a component reassigns its own `this.state`. ' +
755
+ 'Please file an issue.',
756
+ getComponentName(finishedWork.type) || 'instance',
757
);
758
}
759
}
713
-
714
- if (enableProfilerCommitHooks) {
715
- if (
716
- (flags & OnCommitFlag) !== NoFlags &&
717
- typeof onCommit === 'function'
718
- ) {
719
- if (enableSchedulerTracing) {
720
- onCommit(
721
- finishedWork.memoizedProps.id,
722
- current === null ? 'mount' : 'update',
723
- effectDuration,
724
- commitTime,
725
- finishedRoot.memoizedInteractions,
726
- );
727
- } else {
728
- onCommit(
729
- finishedWork.memoizedProps.id,
730
- current === null ? 'mount' : 'update',
731
- effectDuration,
732
- commitTime,
733
- );
734
- }
760
+ }
761
+ if (
762
+ enableProfilerTimer &&
763
+ enableProfilerCommitHooks &&
764
+ finishedWork.mode & ProfileMode
765
+ ) {
766
+ try {
767
+ startLayoutEffectTimer();
768
+ instance.componentDidMount();
769
+ } finally {
770
+ recordLayoutEffectDuration(finishedWork);
771
+ }
772
+ } else {
773
+ instance.componentDidMount();
774
+ }
775
+ } else {
776
+ const prevProps =
777
+ finishedWork.elementType === finishedWork.type
778
+ ? current.memoizedProps
779
+ : resolveDefaultProps(finishedWork.type, current.memoizedProps);
780
+ const prevState = current.memoizedState;
781
+ // We could update instance props and state here,
782
+ // but instead we rely on them being set during last render.
783
+ // TODO: revisit this when we implement resuming.
784
+ if (__DEV__) {
785
+ if (
786
+ finishedWork.type === finishedWork.elementType &&
787
+ !didWarnAboutReassigningProps
788
+ ) {
789
+ if (instance.props !== finishedWork.memoizedProps) {
790
+ console.error(
791
+ 'Expected %s props to match memoized props before ' +
792
+ 'componentDidUpdate. ' +
793
+ 'This might either be because of a bug in React, or because ' +
794
+ 'a component reassigns its own `this.props`. ' +
795
+ 'Please file an issue.',
796
+ getComponentName(finishedWork.type) || 'instance',
797
+ );
798
}
736
-
737
- // Propagate layout effect durations to the next nearest Profiler ancestor.
738
- // Do not reset these values until the next render so DevTools has a chance to read them first.
739
- // TODO: Use JS Stack instead
740
- let parentFiber = finishedWork.return;
741
- while (parentFiber !== null) {
742
- if (parentFiber.tag === Profiler) {
743
- const parentStateNode = parentFiber.stateNode;
744
- parentStateNode.effectDuration += effectDuration;
745
- break;
746
- }
747
- parentFiber = parentFiber.return;
799
+ if (instance.state !== finishedWork.memoizedState) {
800
+ console.error(
801
+ 'Expected %s state to match memoized state before ' +
802
+ 'componentDidUpdate. ' +
803
+ 'This might either be because of a bug in React, or because ' +
804
+ 'a component reassigns its own `this.state`. ' +
805
+ 'Please file an issue.',
806
+ getComponentName(finishedWork.type) || 'instance',
807
+ );
808
}
809
}
810
}
751
- return;
811
+ if (
812
+ enableProfilerTimer &&
813
+ enableProfilerCommitHooks &&
814
+ finishedWork.mode & ProfileMode
815
+ ) {
816
+ try {
817
+ startLayoutEffectTimer();
818
+ instance.componentDidUpdate(
819
+ prevProps,
820
+ prevState,
821
+ instance.__reactInternalSnapshotBeforeUpdate,
822
+ );
823
+ } finally {
824
+ recordLayoutEffectDuration(finishedWork);
825
+ }
826
+ } else {
827
+ instance.componentDidUpdate(
828
+ prevProps,
829
+ prevState,
830
+ instance.__reactInternalSnapshotBeforeUpdate,
831
+ );
832
+ }
833
}
753
- case SuspenseComponent: {
754
- commitSuspenseHydrationCallbacks(finishedRoot, finishedWork);
755
- return;
834
+ }
835
+
836
+ // TODO: I think this is now always non-null by the time it reaches the
837
+ // commit phase. Consider removing the type check.
838
+ const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
839
+ if (updateQueue !== null) {
840
+ if (__DEV__) {
841
+ if (
842
+ finishedWork.type === finishedWork.elementType &&
843
+ !didWarnAboutReassigningProps
844
+ ) {
845
+ if (instance.props !== finishedWork.memoizedProps) {
846
+ console.error(
847
+ 'Expected %s props to match memoized props before ' +
848
+ 'processing the update queue. ' +
849
+ 'This might either be because of a bug in React, or because ' +
850
+ 'a component reassigns its own `this.props`. ' +
851
+ 'Please file an issue.',
852
+ getComponentName(finishedWork.type) || 'instance',
853
+ );
854
+ }
855
+ if (instance.state !== finishedWork.memoizedState) {
856
+ console.error(
857
+ 'Expected %s state to match memoized state before ' +
858
+ 'processing the update queue. ' +
859
+ 'This might either be because of a bug in React, or because ' +
860
+ 'a component reassigns its own `this.state`. ' +
861
+ 'Please file an issue.',
862
+ getComponentName(finishedWork.type) || 'instance',
863
+ );
864
+ }
865
+ }
866
}
757
- case SuspenseListComponent:
758
- case IncompleteClassComponent:
759
- case FundamentalComponent:
760
- case ScopeComponent:
761
- case OffscreenComponent:
762
- case LegacyHiddenComponent:
763
- return;
867
+ // We could update instance props and state here,
868
+ // but instead we rely on them being set during last render.
869
+ // TODO: revisit this when we implement resuming.
870
+ commitUpdateQueue(finishedWork, updateQueue, instance);
871
+ }
872
+}
873
+
874
+function commitLayoutEffectsForHostRoot(finishedWork: Fiber) {
875
+ // TODO: I think this is now always non-null by the time it reaches the
876
+ // commit phase. Consider removing the type check.
877
+ const updateQueue: UpdateQueue<*> | null = (finishedWork.updateQueue: any);
878
+ if (updateQueue !== null) {
879
+ let instance = null;
880
+ if (finishedWork.child !== null) {
881
+ switch (finishedWork.child.tag) {
882
+ case HostComponent:
883
+ instance = getPublicInstance(finishedWork.child.stateNode);
884
+ break;
885
+ case ClassComponent:
886
+ instance = finishedWork.child.stateNode;
887
+ break;
888
+ }
889
+ }
890
+ commitUpdateQueue(finishedWork, updateQueue, instance);
891
+ }
892
+}
893
+
894
+function commitLayoutEffectsForHostComponent(finishedWork: Fiber) {
895
+ const instance: Instance = finishedWork.stateNode;
896
+ const current = finishedWork.alternate;
897
+
898
+ // Renderers may schedule work to be done after host components are mounted
899
+ // (eg DOM renderer may schedule auto-focus for inputs and form controls).
900
+ // These effects should only be committed when components are first mounted,
901
+ // aka when there is no current/alternate.
902
+ if (current === null && finishedWork.flags & Update) {
903
+ const type = finishedWork.type;
904
+ const props = finishedWork.memoizedProps;
905
+ commitMount(instance, type, props, finishedWork);
906
}
765
- invariant(
766
- false,
767
- 'This unit of work tag should not have side-effects. This error is ' +
768
- 'likely caused by a bug in React. Please file an issue.',
769
- );
907
}
908
909
function hideOrUnhideAllChildren(finishedWork, isHidden) {
@@ -1790,7 +1927,7 @@ function commitResetTextContent(current: Fiber): void {
1927
resetTextContent(current.stateNode);
1928
}
1929
1793
-function commitPassiveUnmountInsideDeletedTree(finishedWork: Fiber): void {
1930
+function commitPassiveUnmount(finishedWork: Fiber): void {
1931
switch (finishedWork.tag) {
1932
case FunctionComponent:
1933
case ForwardRef:
@@ -1820,7 +1957,7 @@ function commitPassiveUnmountInsideDeletedTree(finishedWork: Fiber): void {
1957
}
1958
}
1959
1823
-function commitPassiveUnmount(
1960
+function commitPassiveUnmountInsideDeletedTree(
1961
current: Fiber,
1962
nearestMountedAncestor: Fiber | null,
1963
): void {
@@ -2016,7 +2153,6 @@ export {
2153
commitPlacement,
2154
commitDeletion,
2155
commitWork,
2019
- commitLifeCycles,
2156
commitAttachRef,
2157
commitDetachRef,
2158
commitPassiveUnmount,
@@ -2026,4 +2162,5 @@ export {
2162
invokeLayoutEffectUnmountInDEV,
2163
invokePassiveEffectMountInDEV,
2164
invokePassiveEffectUnmountInDEV,
2165
+ recursivelyCommitLayoutEffects,
2166
};
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+54
-76
@@ -22,6 +22,7 @@ import {
22
enableSuspenseServerRenderer,
23
replayFailedUnitOfWorkWithInvokeGuardedCallback,
24
enableProfilerTimer,
25
+ enableProfilerCommitHooks,
26
enableSchedulerTracing,
27
warnAboutUnmockedScheduler,
28
deferRenderPhaseUpdateToNextBatch,
@@ -116,6 +117,7 @@ import {
117
SimpleMemoComponent,
118
Block,
119
ScopeComponent,
120
+ Profiler,
121
} from './ReactWorkTags';
122
import {LegacyRoot} from './ReactRootTags';
123
import {
@@ -126,7 +128,6 @@ import {
128
Ref,
129
ContentReset,
130
Snapshot,
129
- Callback,
131
Passive,
132
PassiveStatic,
133
Incomplete,
@@ -190,7 +191,6 @@ import {
191
} from './ReactFiberThrow.new';
192
import {
193
commitBeforeMutationLifeCycles as commitBeforeMutationEffectOnFiber,
193
- commitLifeCycles as commitLayoutEffectOnFiber,
194
commitPlacement,
195
commitWork,
196
commitDeletion,
@@ -205,6 +205,7 @@ import {
205
invokePassiveEffectMountInDEV,
206
invokeLayoutEffectUnmountInDEV,
207
invokePassiveEffectUnmountInDEV,
208
+ recursivelyCommitLayoutEffects,
209
} from './ReactFiberCommitWork.new';
210
import {enqueueUpdate} from './ReactUpdateQueue.new';
211
import {resetContextDependencies} from './ReactFiberNewContext.new';
@@ -325,6 +326,9 @@ let workInProgressRootRenderTargetTime: number = Infinity;
326
// suspense heuristics and opt out of rendering more content.
327
const RENDER_TIMEOUT_MS = 500;
328
329
+// Used to avoid traversing the return path to find the nearest Profiler ancestor during commit.
330
+let nearestProfilerOnStack: Fiber | null = null;
331
+
332
function resetRenderTimer() {
333
workInProgressRootRenderTargetTime = now() + RENDER_TIMEOUT_MS;
334
}
@@ -1937,7 +1941,27 @@ function commitRootImpl(root, renderPriorityLevel) {
1941
markLayoutEffectsStarted(lanes);
1942
}
1943
1940
- commitLayoutEffects(finishedWork, root, lanes);
1944
+ if (__DEV__) {
1945
+ setCurrentDebugFiberInDEV(finishedWork);
1946
+ invokeGuardedCallback(
1947
+ null,
1948
+ recursivelyCommitLayoutEffects,
1949
+ null,
1950
+ finishedWork,
1951
+ root,
1952
+ );
1953
+ if (hasCaughtError()) {
1954
+ const error = clearCaughtError();
1955
+ captureCommitPhaseErrorOnRoot(finishedWork, finishedWork, error);
1956
+ }
1957
+ resetCurrentDebugFiberInDEV();
1958
+ } else {
1959
+ try {
1960
+ recursivelyCommitLayoutEffects(finishedWork, root);
1961
+ } catch (error) {
1962
+ captureCommitPhaseErrorOnRoot(finishedWork, finishedWork, error);
1963
+ }
1964
+ }
1965
1966
if (__DEV__) {
1967
if (enableDebugTracing) {
@@ -2253,8 +2277,7 @@ function commitMutationEffectsImpl(
2277
commitDetachRef(current);
2278
}
2279
if (enableScopeAPI) {
2256
- // TODO: This is a temporary solution that allowed us to transition away
2257
- // from React Flare on www.
2280
+ // TODO: This is a temporary solution that allowed us to transition away from React Flare on www.
2281
if (fiber.tag === ScopeComponent) {
2282
commitAttachRef(fiber);
2283
}
@@ -2355,75 +2378,6 @@ export function schedulePassiveEffectCallback() {
2378
}
2379
}
2380
2358
-function commitLayoutEffects(
2359
- firstChild: Fiber,
2360
- root: FiberRoot,
2361
- committedLanes: Lanes,
2362
-) {
2363
- let fiber = firstChild;
2364
- while (fiber !== null) {
2365
- if (fiber.child !== null) {
2366
- const primarySubtreeFlags = fiber.subtreeFlags & LayoutMask;
2367
- if (primarySubtreeFlags !== NoFlags) {
2368
- commitLayoutEffects(fiber.child, root, committedLanes);
2369
- }
2370
- }
2371
-
2372
- if (__DEV__) {
2373
- setCurrentDebugFiberInDEV(fiber);
2374
- invokeGuardedCallback(
2375
- null,
2376
- commitLayoutEffectsImpl,
2377
- null,
2378
- fiber,
2379
- root,
2380
- committedLanes,
2381
- );
2382
- if (hasCaughtError()) {
2383
- const error = clearCaughtError();
2384
- captureCommitPhaseError(fiber, fiber.return, error);
2385
- }
2386
- resetCurrentDebugFiberInDEV();
2387
- } else {
2388
- try {
2389
- commitLayoutEffectsImpl(fiber, root, committedLanes);
2390
- } catch (error) {
2391
- captureCommitPhaseError(fiber, fiber.return, error);
2392
- }
2393
- }
2394
- fiber = fiber.sibling;
2395
- }
2396
-}
2397
-
2398
-function commitLayoutEffectsImpl(
2399
- fiber: Fiber,
2400
- root: FiberRoot,
2401
- committedLanes: Lanes,
2402
-) {
2403
- const flags = fiber.flags;
2404
-
2405
- setCurrentDebugFiberInDEV(fiber);
2406
-
2407
- if (flags & (Update | Callback)) {
2408
- const current = fiber.alternate;
2409
- commitLayoutEffectOnFiber(root, current, fiber, committedLanes);
2410
- }
2411
-
2412
- if (enableScopeAPI) {
2413
- // TODO: This is a temporary solution that allowed us to transition away
2414
- // from React Flare on www.
2415
- if (flags & Ref && fiber.tag !== ScopeComponent) {
2416
- commitAttachRef(fiber);
2417
- }
2418
- } else {
2419
- if (flags & Ref) {
2420
- commitAttachRef(fiber);
2421
- }
2422
- }
2423
-
2424
- resetCurrentDebugFiberInDEV();
2425
-}
2426
-
2381
export function flushPassiveEffects(): boolean {
2382
// Returns whether passive effects were flushed.
2383
if (pendingPassiveEffectsRenderPriority !== NoSchedulerPriority) {
@@ -2452,6 +2406,14 @@ export function flushPassiveEffects(): boolean {
2406
function flushPassiveMountEffects(root, firstChild: Fiber): void {
2407
let fiber = firstChild;
2408
while (fiber !== null) {
2409
+ let prevProfilerOnStack = null;
2410
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
2411
+ if (fiber.tag === Profiler) {
2412
+ prevProfilerOnStack = nearestProfilerOnStack;
2413
+ nearestProfilerOnStack = fiber;
2414
+ }
2415
+ }
2416
+
2417
const primarySubtreeFlags = fiber.subtreeFlags & PassiveMask;
2418
2419
if (fiber.child !== null && primarySubtreeFlags !== NoFlags) {
@@ -2482,6 +2444,19 @@ function flushPassiveMountEffects(root, firstChild: Fiber): void {
2444
}
2445
}
2446
2447
+ if (enableProfilerTimer && enableProfilerCommitHooks) {
2448
+ if (fiber.tag === Profiler) {
2449
+ // Bubble times to the next nearest ancestor Profiler.
2450
+ // After we process that Profiler, we'll bubble further up.
2451
+ if (prevProfilerOnStack !== null) {
2452
+ prevProfilerOnStack.stateNode.passiveEffectDuration +=
2453
+ fiber.stateNode.passiveEffectDuration;
2454
+ }
2455
+
2456
+ nearestProfilerOnStack = prevProfilerOnStack;
2457
+ }
2458
+ }
2459
+
2460
fiber = fiber.sibling;
2461
}
2462
}
@@ -2515,7 +2490,7 @@ function flushPassiveUnmountEffects(firstChild: Fiber): void {
2490
const primaryFlags = fiber.flags & Passive;
2491
if (primaryFlags !== NoFlags) {
2492
setCurrentDebugFiberInDEV(fiber);
2518
- commitPassiveUnmountInsideDeletedTreeOnFiber(fiber);
2493
+ commitPassiveUnmountOnFiber(fiber);
2494
resetCurrentDebugFiberInDEV();
2495
}
2496
@@ -2544,7 +2519,10 @@ function flushPassiveUnmountEffectsInsideOfDeletedTree(
2519
2520
if ((fiberToDelete.flags & PassiveStatic) !== NoFlags) {
2521
setCurrentDebugFiberInDEV(fiberToDelete);
2547
- commitPassiveUnmountOnFiber(fiberToDelete, nearestMountedAncestor);
2522
+ commitPassiveUnmountInsideDeletedTreeOnFiber(
2523
+ fiberToDelete,
2524
+ nearestMountedAncestor,
2525
+ );
2526
resetCurrentDebugFiberInDEV();
2527
}
2528
}