Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Effects list refactor continued: passive effects traversal #19374

Merged
merged 11 commits into from
Jul 29, 2020
Next Next commit
Effects list refactor continued: passive effects traversal
* Adds new Passive subtree tag value.
* Bubbles passive flag to ancestors in the case of an unmount.
* Adds recursive traversal for passive effects (mounts and unmounts).
* Removes pendingPassiveHookEffectsMount and pendingPassiveHookEffectsUnmount arrays from work loop.
* Re-adds sibling and child pointer detaching (temporarily removed in previous PR).
* Addresses some minor TODO comments left over from previous PRs.
  • Loading branch information
Brian Vaughn committed Jul 29, 2020
commit bfe385ff4c8ff4d22bd6263c79b2316e17604944
72 changes: 44 additions & 28 deletions packages/react-reconciler/src/ReactFiberCommitWork.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,8 @@ import {
Placement,
Snapshot,
Update,
Passive,
PassiveUnmountPendingDev,
} from './ReactSideEffectTags';
import getComponentName from 'shared/getComponentName';
import invariant from 'shared/invariant';
Expand Down Expand Up @@ -115,9 +117,8 @@ import {
captureCommitPhaseError,
resolveRetryWakeable,
markCommitTimeOfFallback,
enqueuePendingPassiveHookEffectMount,
enqueuePendingPassiveHookEffectUnmount,
enqueuePendingPassiveProfilerEffect,
schedulePassiveEffectCallback,
} from './ReactFiberWorkLoop.new';
import {
NoEffect as NoHookEffect,
Expand All @@ -130,6 +131,10 @@ import {
updateDeprecatedEventListeners,
unmountDeprecatedResponderListeners,
} from './ReactFiberDeprecatedEvents.new';
import {
NoEffect as NoSubtreeTag,
Passive as PassiveSubtreeTag,
} from './ReactSubtreeTags';

let didWarnAboutUndefinedSnapshotBeforeUpdate: Set<mixed> | null = null;
if (__DEV__) {
Expand Down Expand Up @@ -381,26 +386,6 @@ function commitHookEffectListMount(tag: number, finishedWork: Fiber) {
}
}

function schedulePassiveEffects(finishedWork: Fiber) {
const updateQueue: FunctionComponentUpdateQueue | null = (finishedWork.updateQueue: any);
const lastEffect = updateQueue !== null ? updateQueue.lastEffect : null;
if (lastEffect !== null) {
const firstEffect = lastEffect.next;
let effect = firstEffect;
do {
const {next, tag} = effect;
if (
(tag & HookPassive) !== NoHookEffect &&
(tag & HookHasEffect) !== NoHookEffect
) {
enqueuePendingPassiveHookEffectUnmount(finishedWork, effect);
enqueuePendingPassiveHookEffectMount(finishedWork, effect);
}
effect = next;
} while (effect !== firstEffect);
}
}

export function commitPassiveEffectDurations(
finishedRoot: FiberRoot,
finishedWork: Fiber,
Expand Down Expand Up @@ -486,7 +471,9 @@ function commitLifeCycles(
commitHookEffectListMount(HookLayout | HookHasEffect, finishedWork);
}

schedulePassiveEffects(finishedWork);
if ((finishedWork.subtreeTag & PassiveSubtreeTag) !== NoSubtreeTag) {
schedulePassiveEffectCallback();
}
return;
}
case ClassComponent: {
Expand Down Expand Up @@ -892,7 +879,35 @@ function commitUnmount(
const {destroy, tag} = effect;
if (destroy !== undefined) {
if ((tag & HookPassive) !== NoHookEffect) {
enqueuePendingPassiveHookEffectUnmount(current, effect);
effect.tag |= HookHasEffect;
bvaughn marked this conversation as resolved.
Show resolved Hide resolved

// subtreeTags bubble in resetChildLanes which doens't get called for unmounted subtrees.
// So in the case of unmounts, we need to bubble passive effects explicitly.
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
let ancestor = current.return;
while (ancestor !== null) {
ancestor.subtreeTag |= PassiveSubtreeTag;
const alternate = ancestor.alternate;
if (alternate !== null) {
alternate.subtreeTag |= PassiveSubtreeTag;
}

ancestor = ancestor.return;
}

current.effectTag |= Passive;
bvaughn marked this conversation as resolved.
Show resolved Hide resolved

if (__DEV__) {
// This flag is used to avoid warning about an update to an unmounted component
// if the component has a passive unmount scheduled.
// Presumably the listener would be cleaned up by that unmount.
current.effectTag |= PassiveUnmountPendingDev;
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
const alternate = current.alternate;
if (alternate !== null) {
alternate.effectTag |= PassiveUnmountPendingDev;
}
}

schedulePassiveEffectCallback();
bvaughn marked this conversation as resolved.
Show resolved Hide resolved
} else {
if (
enableProfilerTimer &&
Expand Down Expand Up @@ -1013,8 +1028,11 @@ function commitNestedUnmounts(
}

function detachFiberMutation(fiber: Fiber) {
// Cut off the return pointers to disconnect it from the tree. Ideally, we
// should clear the child pointer of the parent alternate to let this
// Cut off the return pointers to disconnect it from the tree.
// Note that we can't clear child or sibling pointers yet,
// because they may be required for passive effects.
// These pointers will be cleared in a separate pass.
// Ideally, we should clear the child pointer of the parent alternate to let this
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If a Deletion always gets a passive effect scheduled on it, as suggested above, we could move this whole function to the passive phase. (Except maybe return and alternate, since those affect semantics, like if you call setState on an unmounted component.)

Copy link
Contributor Author

@bvaughn bvaughn Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we don't clear return during layout, it would break a few things:

  • The unmounted state warning
  • How events bubble for the new API Dominic has been working on

That being said, it would fix a limitation of the Profiler API when it comes to measuring time spent in passive effect cleanup. That time currently doesn't get reported during an unmount (either a regular unmount or because of an error boundary) because the return pointer has been detached previously and so it can't bubble up to the nearest Profiler node. If we deferred detaching it until after passive, that time would be reported which is a plus.

Not sure if there would be a viable workaround for the event case, but for the unmounted state warning- we could walk the return path and look for Fibers with Deletion effects scheduled, except that this would have false positives for deletions that were scheduled and not committed so that's a bummer.

I guess we could always set another bit to mark a Fiber as having been deleted but at that point we aren't really saving much (aside from the Profiler fix).

Copy link
Contributor Author

@bvaughn bvaughn Jul 29, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That being said, I believe we can safely defer everything except the return pointer cleanup to the passive effects phase which would be slightly better.

7d618ed

// get GC:ed but we don't know which for sure which parent is the current
// one so we'll settle for GC:ing the subtree of this child. This child
// itself will be GC:ed when the parent updates the next time.
Expand All @@ -1023,7 +1041,6 @@ function detachFiberMutation(fiber: Fiber) {
// traversal in a later effect. See PR #16820. We now clear the sibling
// field after effects, see: detachFiberAfterEffects.
fiber.alternate = null;
fiber.child = null;
fiber.dependencies = null;
fiber.firstEffect = null;
fiber.lastEffect = null;
Expand All @@ -1032,7 +1049,6 @@ function detachFiberMutation(fiber: Fiber) {
fiber.pendingProps = null;
fiber.return = null;
fiber.stateNode = null;
fiber.updateQueue = null;
if (__DEV__) {
fiber._debugOwner = null;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ function deleteHydratableInstance(
const childToDelete = createFiberFromHostInstanceForDeletion();
childToDelete.stateNode = instance;
childToDelete.return = returnFiber;
childToDelete.effectTag = Deletion;

const deletions = returnFiber.deletions;
if (deletions === null) {
returnFiber.deletions = [childToDelete];
Expand Down
Loading