@samitouri / QOS-React-2 / commits / 9198a5cec0

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 }