1411 lines
48 KiB
JavaScript
1411 lines
48 KiB
JavaScript
/**
|
|
* Copyright (c) Facebook, Inc. and its affiliates.
|
|
*
|
|
* This source code is licensed under the MIT license found in the
|
|
* LICENSE file in the root directory of this source tree.
|
|
*
|
|
* @flow
|
|
*/
|
|
|
|
import type {Fiber} from './ReactFiber';
|
|
import type {ExpirationTime} from './ReactFiberExpirationTime';
|
|
import type {
|
|
ReactEventResponder,
|
|
ReactEventResponderInstance,
|
|
ReactFundamentalComponentInstance,
|
|
ReactEventResponderListener,
|
|
} from 'shared/ReactTypes';
|
|
import type {FiberRoot} from './ReactFiberRoot';
|
|
import type {
|
|
Instance,
|
|
Type,
|
|
Props,
|
|
Container,
|
|
ChildSet,
|
|
} from './ReactFiberHostConfig';
|
|
import type {
|
|
SuspenseState,
|
|
SuspenseListRenderState,
|
|
} from './ReactFiberSuspenseComponent';
|
|
import type {SuspenseContext} from './ReactFiberSuspenseContext';
|
|
|
|
import {now} from './SchedulerWithReactIntegration';
|
|
|
|
import {REACT_RESPONDER_TYPE} from 'shared/ReactSymbols';
|
|
import {
|
|
IndeterminateComponent,
|
|
FunctionComponent,
|
|
ClassComponent,
|
|
HostRoot,
|
|
HostComponent,
|
|
HostText,
|
|
HostPortal,
|
|
ContextProvider,
|
|
ContextConsumer,
|
|
ForwardRef,
|
|
Fragment,
|
|
Mode,
|
|
Profiler,
|
|
SuspenseComponent,
|
|
SuspenseListComponent,
|
|
DehydratedSuspenseComponent,
|
|
MemoComponent,
|
|
SimpleMemoComponent,
|
|
LazyComponent,
|
|
IncompleteClassComponent,
|
|
FundamentalComponent,
|
|
} from 'shared/ReactWorkTags';
|
|
import {NoMode, BatchedMode} from './ReactTypeOfMode';
|
|
import {
|
|
Placement,
|
|
Ref,
|
|
Update,
|
|
NoEffect,
|
|
DidCapture,
|
|
Deletion,
|
|
} from 'shared/ReactSideEffectTags';
|
|
import invariant from 'shared/invariant';
|
|
|
|
import {
|
|
createInstance,
|
|
createTextInstance,
|
|
appendInitialChild,
|
|
finalizeInitialChildren,
|
|
prepareUpdate,
|
|
supportsMutation,
|
|
supportsPersistence,
|
|
cloneInstance,
|
|
cloneHiddenInstance,
|
|
cloneHiddenTextInstance,
|
|
createContainerChildSet,
|
|
appendChildToContainerChildSet,
|
|
finalizeContainerChildren,
|
|
mountResponderInstance,
|
|
unmountResponderInstance,
|
|
getFundamentalComponentInstance,
|
|
mountFundamentalComponent,
|
|
cloneFundamentalInstance,
|
|
shouldUpdateFundamentalComponent,
|
|
} from './ReactFiberHostConfig';
|
|
import {
|
|
getRootHostContainer,
|
|
popHostContext,
|
|
getHostContext,
|
|
popHostContainer,
|
|
} from './ReactFiberHostContext';
|
|
import {NoWork} from './ReactFiberExpirationTime';
|
|
import {createResponderInstance} from './ReactFiberEvents';
|
|
import {
|
|
suspenseStackCursor,
|
|
InvisibleParentSuspenseContext,
|
|
hasSuspenseContext,
|
|
popSuspenseContext,
|
|
pushSuspenseContext,
|
|
setShallowSuspenseContext,
|
|
ForceSuspenseFallback,
|
|
setDefaultShallowSuspenseContext,
|
|
} from './ReactFiberSuspenseContext';
|
|
import {findFirstSuspended} from './ReactFiberSuspenseComponent';
|
|
import {
|
|
isContextProvider as isLegacyContextProvider,
|
|
popContext as popLegacyContext,
|
|
popTopLevelContextObject as popTopLevelLegacyContextObject,
|
|
} from './ReactFiberContext';
|
|
import {popProvider} from './ReactFiberNewContext';
|
|
import {
|
|
prepareToHydrateHostInstance,
|
|
prepareToHydrateHostTextInstance,
|
|
skipPastDehydratedSuspenseInstance,
|
|
popHydrationState,
|
|
resetHydrationState,
|
|
} from './ReactFiberHydrationContext';
|
|
import {
|
|
enableSchedulerTracing,
|
|
enableSuspenseCallback,
|
|
enableSuspenseServerRenderer,
|
|
enableFlareAPI,
|
|
enableFundamentalAPI,
|
|
} from 'shared/ReactFeatureFlags';
|
|
import {
|
|
markSpawnedWork,
|
|
renderDidSuspend,
|
|
renderDidSuspendDelayIfPossible,
|
|
renderHasNotSuspendedYet,
|
|
} from './ReactFiberWorkLoop';
|
|
import {createFundamentalStateInstance} from './ReactFiberFundamental';
|
|
import {Never} from './ReactFiberExpirationTime';
|
|
import {resetChildFibers} from './ReactChildFiber';
|
|
import warning from 'shared/warning';
|
|
|
|
const emptyObject = {};
|
|
const isArray = Array.isArray;
|
|
|
|
function markUpdate(workInProgress: Fiber) {
|
|
// Tag the fiber with an update effect. This turns a Placement into
|
|
// a PlacementAndUpdate.
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
|
|
function markRef(workInProgress: Fiber) {
|
|
workInProgress.effectTag |= Ref;
|
|
}
|
|
|
|
let appendAllChildren;
|
|
let updateHostContainer;
|
|
let updateHostComponent;
|
|
let updateHostText;
|
|
if (supportsMutation) {
|
|
// Mutation mode
|
|
|
|
appendAllChildren = function(
|
|
parent: Instance,
|
|
workInProgress: Fiber,
|
|
needsVisibilityToggle: boolean,
|
|
isHidden: boolean,
|
|
) {
|
|
// We only have the top Fiber that was created but we need recurse down its
|
|
// children to find all the terminal nodes.
|
|
let node = workInProgress.child;
|
|
while (node !== null) {
|
|
if (node.tag === HostComponent || node.tag === HostText) {
|
|
appendInitialChild(parent, node.stateNode);
|
|
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
|
|
appendInitialChild(parent, node.stateNode.instance);
|
|
} else if (node.tag === HostPortal) {
|
|
// If we have a portal child, then we don't want to traverse
|
|
// down its children. Instead, we'll get insertions from each child in
|
|
// the portal directly.
|
|
} else if (node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
if (node === workInProgress) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === workInProgress) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
};
|
|
|
|
updateHostContainer = function(workInProgress: Fiber) {
|
|
// Noop
|
|
};
|
|
updateHostComponent = function(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
type: Type,
|
|
newProps: Props,
|
|
rootContainerInstance: Container,
|
|
) {
|
|
// If we have an alternate, that means this is an update and we need to
|
|
// schedule a side-effect to do the updates.
|
|
const oldProps = current.memoizedProps;
|
|
if (oldProps === newProps) {
|
|
// In mutation mode, this is sufficient for a bailout because
|
|
// we won't touch this node even if children changed.
|
|
return;
|
|
}
|
|
|
|
// If we get updated because one of our children updated, we don't
|
|
// have newProps so we'll have to reuse them.
|
|
// TODO: Split the update API as separate for the props vs. children.
|
|
// Even better would be if children weren't special cased at all tho.
|
|
const instance: Instance = workInProgress.stateNode;
|
|
const currentHostContext = getHostContext();
|
|
// TODO: Experiencing an error where oldProps is null. Suggests a host
|
|
// component is hitting the resume path. Figure out why. Possibly
|
|
// related to `hidden`.
|
|
const updatePayload = prepareUpdate(
|
|
instance,
|
|
type,
|
|
oldProps,
|
|
newProps,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
);
|
|
// TODO: Type this specific to this type of component.
|
|
workInProgress.updateQueue = (updatePayload: any);
|
|
// If the update payload indicates that there is a change or if there
|
|
// is a new ref we mark this as an update. All the work is done in commitWork.
|
|
if (updatePayload) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
};
|
|
updateHostText = function(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
oldText: string,
|
|
newText: string,
|
|
) {
|
|
// If the text differs, mark it as an update. All the work in done in commitWork.
|
|
if (oldText !== newText) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
};
|
|
} else if (supportsPersistence) {
|
|
// Persistent host tree mode
|
|
|
|
appendAllChildren = function(
|
|
parent: Instance,
|
|
workInProgress: Fiber,
|
|
needsVisibilityToggle: boolean,
|
|
isHidden: boolean,
|
|
) {
|
|
// We only have the top Fiber that was created but we need recurse down its
|
|
// children to find all the terminal nodes.
|
|
let node = workInProgress.child;
|
|
while (node !== null) {
|
|
// eslint-disable-next-line no-labels
|
|
branches: if (node.tag === HostComponent) {
|
|
let instance = node.stateNode;
|
|
if (needsVisibilityToggle && isHidden) {
|
|
// This child is inside a timed out tree. Hide it.
|
|
const props = node.memoizedProps;
|
|
const type = node.type;
|
|
instance = cloneHiddenInstance(instance, type, props, node);
|
|
}
|
|
appendInitialChild(parent, instance);
|
|
} else if (node.tag === HostText) {
|
|
let instance = node.stateNode;
|
|
if (needsVisibilityToggle && isHidden) {
|
|
// This child is inside a timed out tree. Hide it.
|
|
const text = node.memoizedProps;
|
|
instance = cloneHiddenTextInstance(instance, text, node);
|
|
}
|
|
appendInitialChild(parent, instance);
|
|
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
|
|
let instance = node.stateNode.instance;
|
|
if (needsVisibilityToggle && isHidden) {
|
|
// This child is inside a timed out tree. Hide it.
|
|
const props = node.memoizedProps;
|
|
const type = node.type;
|
|
instance = cloneHiddenInstance(instance, type, props, node);
|
|
}
|
|
appendInitialChild(parent, instance);
|
|
} else if (node.tag === HostPortal) {
|
|
// If we have a portal child, then we don't want to traverse
|
|
// down its children. Instead, we'll get insertions from each child in
|
|
// the portal directly.
|
|
} else if (node.tag === SuspenseComponent) {
|
|
if ((node.effectTag & Update) !== NoEffect) {
|
|
// Need to toggle the visibility of the primary children.
|
|
const newIsHidden = node.memoizedState !== null;
|
|
if (newIsHidden) {
|
|
const primaryChildParent = node.child;
|
|
if (primaryChildParent !== null) {
|
|
if (primaryChildParent.child !== null) {
|
|
primaryChildParent.child.return = primaryChildParent;
|
|
appendAllChildren(
|
|
parent,
|
|
primaryChildParent,
|
|
true,
|
|
newIsHidden,
|
|
);
|
|
}
|
|
const fallbackChildParent = primaryChildParent.sibling;
|
|
if (fallbackChildParent !== null) {
|
|
fallbackChildParent.return = node;
|
|
node = fallbackChildParent;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (node.child !== null) {
|
|
// Continue traversing like normal
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
} else if (node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
// $FlowFixMe This is correct but Flow is confused by the labeled break.
|
|
node = (node: Fiber);
|
|
if (node === workInProgress) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === workInProgress) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
};
|
|
|
|
// An unfortunate fork of appendAllChildren because we have two different parent types.
|
|
const appendAllChildrenToContainer = function(
|
|
containerChildSet: ChildSet,
|
|
workInProgress: Fiber,
|
|
needsVisibilityToggle: boolean,
|
|
isHidden: boolean,
|
|
) {
|
|
// We only have the top Fiber that was created but we need recurse down its
|
|
// children to find all the terminal nodes.
|
|
let node = workInProgress.child;
|
|
while (node !== null) {
|
|
// eslint-disable-next-line no-labels
|
|
branches: if (node.tag === HostComponent) {
|
|
let instance = node.stateNode;
|
|
if (needsVisibilityToggle && isHidden) {
|
|
// This child is inside a timed out tree. Hide it.
|
|
const props = node.memoizedProps;
|
|
const type = node.type;
|
|
instance = cloneHiddenInstance(instance, type, props, node);
|
|
}
|
|
appendChildToContainerChildSet(containerChildSet, instance);
|
|
} else if (node.tag === HostText) {
|
|
let instance = node.stateNode;
|
|
if (needsVisibilityToggle && isHidden) {
|
|
// This child is inside a timed out tree. Hide it.
|
|
const text = node.memoizedProps;
|
|
instance = cloneHiddenTextInstance(instance, text, node);
|
|
}
|
|
appendChildToContainerChildSet(containerChildSet, instance);
|
|
} else if (enableFundamentalAPI && node.tag === FundamentalComponent) {
|
|
let instance = node.stateNode.instance;
|
|
if (needsVisibilityToggle && isHidden) {
|
|
// This child is inside a timed out tree. Hide it.
|
|
const props = node.memoizedProps;
|
|
const type = node.type;
|
|
instance = cloneHiddenInstance(instance, type, props, node);
|
|
}
|
|
appendChildToContainerChildSet(containerChildSet, instance);
|
|
} else if (node.tag === HostPortal) {
|
|
// If we have a portal child, then we don't want to traverse
|
|
// down its children. Instead, we'll get insertions from each child in
|
|
// the portal directly.
|
|
} else if (node.tag === SuspenseComponent) {
|
|
if ((node.effectTag & Update) !== NoEffect) {
|
|
// Need to toggle the visibility of the primary children.
|
|
const newIsHidden = node.memoizedState !== null;
|
|
if (newIsHidden) {
|
|
const primaryChildParent = node.child;
|
|
if (primaryChildParent !== null) {
|
|
if (primaryChildParent.child !== null) {
|
|
primaryChildParent.child.return = primaryChildParent;
|
|
appendAllChildrenToContainer(
|
|
containerChildSet,
|
|
primaryChildParent,
|
|
true,
|
|
newIsHidden,
|
|
);
|
|
}
|
|
const fallbackChildParent = primaryChildParent.sibling;
|
|
if (fallbackChildParent !== null) {
|
|
fallbackChildParent.return = node;
|
|
node = fallbackChildParent;
|
|
continue;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (node.child !== null) {
|
|
// Continue traversing like normal
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
} else if (node.child !== null) {
|
|
node.child.return = node;
|
|
node = node.child;
|
|
continue;
|
|
}
|
|
// $FlowFixMe This is correct but Flow is confused by the labeled break.
|
|
node = (node: Fiber);
|
|
if (node === workInProgress) {
|
|
return;
|
|
}
|
|
while (node.sibling === null) {
|
|
if (node.return === null || node.return === workInProgress) {
|
|
return;
|
|
}
|
|
node = node.return;
|
|
}
|
|
node.sibling.return = node.return;
|
|
node = node.sibling;
|
|
}
|
|
};
|
|
updateHostContainer = function(workInProgress: Fiber) {
|
|
const portalOrRoot: {
|
|
containerInfo: Container,
|
|
pendingChildren: ChildSet,
|
|
} =
|
|
workInProgress.stateNode;
|
|
const childrenUnchanged = workInProgress.firstEffect === null;
|
|
if (childrenUnchanged) {
|
|
// No changes, just reuse the existing instance.
|
|
} else {
|
|
const container = portalOrRoot.containerInfo;
|
|
let newChildSet = createContainerChildSet(container);
|
|
// If children might have changed, we have to add them all to the set.
|
|
appendAllChildrenToContainer(newChildSet, workInProgress, false, false);
|
|
portalOrRoot.pendingChildren = newChildSet;
|
|
// Schedule an update on the container to swap out the container.
|
|
markUpdate(workInProgress);
|
|
finalizeContainerChildren(container, newChildSet);
|
|
}
|
|
};
|
|
updateHostComponent = function(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
type: Type,
|
|
newProps: Props,
|
|
rootContainerInstance: Container,
|
|
) {
|
|
const currentInstance = current.stateNode;
|
|
const oldProps = current.memoizedProps;
|
|
// If there are no effects associated with this node, then none of our children had any updates.
|
|
// This guarantees that we can reuse all of them.
|
|
const childrenUnchanged = workInProgress.firstEffect === null;
|
|
if (childrenUnchanged && oldProps === newProps) {
|
|
// No changes, just reuse the existing instance.
|
|
// Note that this might release a previous clone.
|
|
workInProgress.stateNode = currentInstance;
|
|
return;
|
|
}
|
|
const recyclableInstance: Instance = workInProgress.stateNode;
|
|
const currentHostContext = getHostContext();
|
|
let updatePayload = null;
|
|
if (oldProps !== newProps) {
|
|
updatePayload = prepareUpdate(
|
|
recyclableInstance,
|
|
type,
|
|
oldProps,
|
|
newProps,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
);
|
|
}
|
|
if (childrenUnchanged && updatePayload === null) {
|
|
// No changes, just reuse the existing instance.
|
|
// Note that this might release a previous clone.
|
|
workInProgress.stateNode = currentInstance;
|
|
return;
|
|
}
|
|
let newInstance = cloneInstance(
|
|
currentInstance,
|
|
updatePayload,
|
|
type,
|
|
oldProps,
|
|
newProps,
|
|
workInProgress,
|
|
childrenUnchanged,
|
|
recyclableInstance,
|
|
);
|
|
if (
|
|
finalizeInitialChildren(
|
|
newInstance,
|
|
type,
|
|
newProps,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
)
|
|
) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
workInProgress.stateNode = newInstance;
|
|
if (childrenUnchanged) {
|
|
// If there are no other effects in this tree, we need to flag this node as having one.
|
|
// Even though we're not going to use it for anything.
|
|
// Otherwise parents won't know that there are new children to propagate upwards.
|
|
markUpdate(workInProgress);
|
|
} else {
|
|
// If children might have changed, we have to add them all to the set.
|
|
appendAllChildren(newInstance, workInProgress, false, false);
|
|
}
|
|
};
|
|
updateHostText = function(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
oldText: string,
|
|
newText: string,
|
|
) {
|
|
if (oldText !== newText) {
|
|
// If the text content differs, we'll create a new text instance for it.
|
|
const rootContainerInstance = getRootHostContainer();
|
|
const currentHostContext = getHostContext();
|
|
workInProgress.stateNode = createTextInstance(
|
|
newText,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
workInProgress,
|
|
);
|
|
// We'll have to mark it as having an effect, even though we won't use the effect for anything.
|
|
// This lets the parents know that at least one of their children has changed.
|
|
markUpdate(workInProgress);
|
|
}
|
|
};
|
|
} else {
|
|
// No host operations
|
|
updateHostContainer = function(workInProgress: Fiber) {
|
|
// Noop
|
|
};
|
|
updateHostComponent = function(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
type: Type,
|
|
newProps: Props,
|
|
rootContainerInstance: Container,
|
|
) {
|
|
// Noop
|
|
};
|
|
updateHostText = function(
|
|
current: Fiber,
|
|
workInProgress: Fiber,
|
|
oldText: string,
|
|
newText: string,
|
|
) {
|
|
// Noop
|
|
};
|
|
}
|
|
|
|
function cutOffTailIfNeeded(
|
|
renderState: SuspenseListRenderState,
|
|
hasRenderedATailFallback: boolean,
|
|
) {
|
|
switch (renderState.tailMode) {
|
|
case 'hidden': {
|
|
// Any insertions at the end of the tail list after this point
|
|
// should be invisible. If there are already mounted boundaries
|
|
// anything before them are not considered for collapsing.
|
|
// Therefore we need to go through the whole tail to find if
|
|
// there are any.
|
|
let tailNode = renderState.tail;
|
|
let lastTailNode = null;
|
|
while (tailNode !== null) {
|
|
if (tailNode.alternate !== null) {
|
|
lastTailNode = tailNode;
|
|
}
|
|
tailNode = tailNode.sibling;
|
|
}
|
|
// Next we're simply going to delete all insertions after the
|
|
// last rendered item.
|
|
if (lastTailNode === null) {
|
|
// All remaining items in the tail are insertions.
|
|
renderState.tail = null;
|
|
} else {
|
|
// Detach the insertion after the last node that was already
|
|
// inserted.
|
|
lastTailNode.sibling = null;
|
|
}
|
|
break;
|
|
}
|
|
case 'collapsed': {
|
|
// Any insertions at the end of the tail list after this point
|
|
// should be invisible. If there are already mounted boundaries
|
|
// anything before them are not considered for collapsing.
|
|
// Therefore we need to go through the whole tail to find if
|
|
// there are any.
|
|
let tailNode = renderState.tail;
|
|
let lastTailNode = null;
|
|
while (tailNode !== null) {
|
|
if (tailNode.alternate !== null) {
|
|
lastTailNode = tailNode;
|
|
}
|
|
tailNode = tailNode.sibling;
|
|
}
|
|
// Next we're simply going to delete all insertions after the
|
|
// last rendered item.
|
|
if (lastTailNode === null) {
|
|
// All remaining items in the tail are insertions.
|
|
if (!hasRenderedATailFallback && renderState.tail !== null) {
|
|
// We suspended during the head. We want to show at least one
|
|
// row at the tail. So we'll keep on and cut off the rest.
|
|
renderState.tail.sibling = null;
|
|
} else {
|
|
renderState.tail = null;
|
|
}
|
|
} else {
|
|
// Detach the insertion after the last node that was already
|
|
// inserted.
|
|
lastTailNode.sibling = null;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function completeWork(
|
|
current: Fiber | null,
|
|
workInProgress: Fiber,
|
|
renderExpirationTime: ExpirationTime,
|
|
): Fiber | null {
|
|
const newProps = workInProgress.pendingProps;
|
|
|
|
switch (workInProgress.tag) {
|
|
case IndeterminateComponent:
|
|
break;
|
|
case LazyComponent:
|
|
break;
|
|
case SimpleMemoComponent:
|
|
case FunctionComponent:
|
|
break;
|
|
case ClassComponent: {
|
|
const Component = workInProgress.type;
|
|
if (isLegacyContextProvider(Component)) {
|
|
popLegacyContext(workInProgress);
|
|
}
|
|
break;
|
|
}
|
|
case HostRoot: {
|
|
popHostContainer(workInProgress);
|
|
popTopLevelLegacyContextObject(workInProgress);
|
|
const fiberRoot = (workInProgress.stateNode: FiberRoot);
|
|
if (fiberRoot.pendingContext) {
|
|
fiberRoot.context = fiberRoot.pendingContext;
|
|
fiberRoot.pendingContext = null;
|
|
}
|
|
if (current === null || current.child === null) {
|
|
// If we hydrated, pop so that we can delete any remaining children
|
|
// that weren't hydrated.
|
|
popHydrationState(workInProgress);
|
|
// This resets the hacky state to fix isMounted before committing.
|
|
// TODO: Delete this when we delete isMounted and findDOMNode.
|
|
workInProgress.effectTag &= ~Placement;
|
|
}
|
|
updateHostContainer(workInProgress);
|
|
break;
|
|
}
|
|
case HostComponent: {
|
|
popHostContext(workInProgress);
|
|
const rootContainerInstance = getRootHostContainer();
|
|
const type = workInProgress.type;
|
|
if (current !== null && workInProgress.stateNode != null) {
|
|
updateHostComponent(
|
|
current,
|
|
workInProgress,
|
|
type,
|
|
newProps,
|
|
rootContainerInstance,
|
|
);
|
|
|
|
if (enableFlareAPI) {
|
|
const prevListeners = current.memoizedProps.listeners;
|
|
const nextListeners = newProps.listeners;
|
|
const instance = workInProgress.stateNode;
|
|
if (prevListeners !== nextListeners) {
|
|
updateEventListeners(
|
|
nextListeners,
|
|
instance,
|
|
rootContainerInstance,
|
|
workInProgress,
|
|
);
|
|
}
|
|
}
|
|
|
|
if (current.ref !== workInProgress.ref) {
|
|
markRef(workInProgress);
|
|
}
|
|
} else {
|
|
if (!newProps) {
|
|
invariant(
|
|
workInProgress.stateNode !== null,
|
|
'We must have new props for new mounts. This error is likely ' +
|
|
'caused by a bug in React. Please file an issue.',
|
|
);
|
|
// This can happen when we abort work.
|
|
break;
|
|
}
|
|
|
|
const currentHostContext = getHostContext();
|
|
// TODO: Move createInstance to beginWork and keep it on a context
|
|
// "stack" as the parent. Then append children as we go in beginWork
|
|
// or completeWork depending on we want to add then top->down or
|
|
// bottom->up. Top->down is faster in IE11.
|
|
let wasHydrated = popHydrationState(workInProgress);
|
|
if (wasHydrated) {
|
|
// TODO: Move this and createInstance step into the beginPhase
|
|
// to consolidate.
|
|
if (
|
|
prepareToHydrateHostInstance(
|
|
workInProgress,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
)
|
|
) {
|
|
// If changes to the hydrated node needs to be applied at the
|
|
// commit-phase we mark this as such.
|
|
markUpdate(workInProgress);
|
|
}
|
|
if (enableFlareAPI) {
|
|
const instance = workInProgress.stateNode;
|
|
const listeners = newProps.listeners;
|
|
if (listeners != null) {
|
|
updateEventListeners(
|
|
listeners,
|
|
instance,
|
|
rootContainerInstance,
|
|
workInProgress,
|
|
);
|
|
}
|
|
}
|
|
} else {
|
|
let instance = createInstance(
|
|
type,
|
|
newProps,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
workInProgress,
|
|
);
|
|
|
|
appendAllChildren(instance, workInProgress, false, false);
|
|
|
|
if (enableFlareAPI) {
|
|
const listeners = newProps.listeners;
|
|
if (listeners != null) {
|
|
updateEventListeners(
|
|
listeners,
|
|
instance,
|
|
rootContainerInstance,
|
|
workInProgress,
|
|
);
|
|
}
|
|
}
|
|
|
|
// Certain renderers require commit-time effects for initial mount.
|
|
// (eg DOM renderer supports auto-focus for certain elements).
|
|
// Make sure such renderers get scheduled for later work.
|
|
if (
|
|
finalizeInitialChildren(
|
|
instance,
|
|
type,
|
|
newProps,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
)
|
|
) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
workInProgress.stateNode = instance;
|
|
}
|
|
|
|
if (workInProgress.ref !== null) {
|
|
// If there is a ref on a host node we need to schedule a callback
|
|
markRef(workInProgress);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case HostText: {
|
|
let newText = newProps;
|
|
if (current && workInProgress.stateNode != null) {
|
|
const oldText = current.memoizedProps;
|
|
// If we have an alternate, that means this is an update and we need
|
|
// to schedule a side-effect to do the updates.
|
|
updateHostText(current, workInProgress, oldText, newText);
|
|
} else {
|
|
if (typeof newText !== 'string') {
|
|
invariant(
|
|
workInProgress.stateNode !== null,
|
|
'We must have new props for new mounts. This error is likely ' +
|
|
'caused by a bug in React. Please file an issue.',
|
|
);
|
|
// This can happen when we abort work.
|
|
}
|
|
const rootContainerInstance = getRootHostContainer();
|
|
const currentHostContext = getHostContext();
|
|
let wasHydrated = popHydrationState(workInProgress);
|
|
if (wasHydrated) {
|
|
if (prepareToHydrateHostTextInstance(workInProgress)) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
} else {
|
|
workInProgress.stateNode = createTextInstance(
|
|
newText,
|
|
rootContainerInstance,
|
|
currentHostContext,
|
|
workInProgress,
|
|
);
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case ForwardRef:
|
|
break;
|
|
case SuspenseComponent: {
|
|
popSuspenseContext(workInProgress);
|
|
const nextState: null | SuspenseState = workInProgress.memoizedState;
|
|
if ((workInProgress.effectTag & DidCapture) !== NoEffect) {
|
|
// Something suspended. Re-render with the fallback children.
|
|
workInProgress.expirationTime = renderExpirationTime;
|
|
// Do not reset the effect list.
|
|
return workInProgress;
|
|
}
|
|
|
|
const nextDidTimeout = nextState !== null;
|
|
let prevDidTimeout = false;
|
|
if (current === null) {
|
|
// In cases where we didn't find a suitable hydration boundary we never
|
|
// downgraded this to a DehydratedSuspenseComponent, but we still need to
|
|
// pop the hydration state since we might be inside the insertion tree.
|
|
popHydrationState(workInProgress);
|
|
} else {
|
|
const prevState: null | SuspenseState = current.memoizedState;
|
|
prevDidTimeout = prevState !== null;
|
|
if (!nextDidTimeout && prevState !== null) {
|
|
// We just switched from the fallback to the normal children.
|
|
// Delete the fallback.
|
|
// TODO: Would it be better to store the fallback fragment on
|
|
// the stateNode during the begin phase?
|
|
const currentFallbackChild: Fiber | null = (current.child: any)
|
|
.sibling;
|
|
if (currentFallbackChild !== null) {
|
|
// Deletions go at the beginning of the return fiber's effect list
|
|
const first = workInProgress.firstEffect;
|
|
if (first !== null) {
|
|
workInProgress.firstEffect = currentFallbackChild;
|
|
currentFallbackChild.nextEffect = first;
|
|
} else {
|
|
workInProgress.firstEffect = workInProgress.lastEffect = currentFallbackChild;
|
|
currentFallbackChild.nextEffect = null;
|
|
}
|
|
currentFallbackChild.effectTag = Deletion;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (nextDidTimeout && !prevDidTimeout) {
|
|
// If this subtreee is running in batched mode we can suspend,
|
|
// otherwise we won't suspend.
|
|
// TODO: This will still suspend a synchronous tree if anything
|
|
// in the concurrent tree already suspended during this render.
|
|
// This is a known bug.
|
|
if ((workInProgress.mode & BatchedMode) !== NoMode) {
|
|
// TODO: Move this back to throwException because this is too late
|
|
// if this is a large tree which is common for initial loads. We
|
|
// don't know if we should restart a render or not until we get
|
|
// this marker, and this is too late.
|
|
// If this render already had a ping or lower pri updates,
|
|
// and this is the first time we know we're going to suspend we
|
|
// should be able to immediately restart from within throwException.
|
|
const hasInvisibleChildContext =
|
|
current === null &&
|
|
workInProgress.memoizedProps.unstable_avoidThisFallback !== true;
|
|
if (
|
|
hasInvisibleChildContext ||
|
|
hasSuspenseContext(
|
|
suspenseStackCursor.current,
|
|
(InvisibleParentSuspenseContext: SuspenseContext),
|
|
)
|
|
) {
|
|
// If this was in an invisible tree or a new render, then showing
|
|
// this boundary is ok.
|
|
renderDidSuspend();
|
|
} else {
|
|
// Otherwise, we're going to have to hide content so we should
|
|
// suspend for longer if possible.
|
|
renderDidSuspendDelayIfPossible();
|
|
}
|
|
}
|
|
}
|
|
|
|
if (supportsPersistence) {
|
|
// TODO: Only schedule updates if not prevDidTimeout.
|
|
if (nextDidTimeout) {
|
|
// If this boundary just timed out, schedule an effect to attach a
|
|
// retry listener to the proimse. This flag is also used to hide the
|
|
// primary children.
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
}
|
|
if (supportsMutation) {
|
|
// TODO: Only schedule updates if these values are non equal, i.e. it changed.
|
|
if (nextDidTimeout || prevDidTimeout) {
|
|
// If this boundary just timed out, schedule an effect to attach a
|
|
// retry listener to the proimse. This flag is also used to hide the
|
|
// primary children. In mutation mode, we also need the flag to
|
|
// *unhide* children that were previously hidden, so check if the
|
|
// is currently timed out, too.
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
}
|
|
if (
|
|
enableSuspenseCallback &&
|
|
workInProgress.updateQueue !== null &&
|
|
workInProgress.memoizedProps.suspenseCallback != null
|
|
) {
|
|
// Always notify the callback
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
break;
|
|
}
|
|
case Fragment:
|
|
break;
|
|
case Mode:
|
|
break;
|
|
case Profiler:
|
|
break;
|
|
case HostPortal:
|
|
popHostContainer(workInProgress);
|
|
updateHostContainer(workInProgress);
|
|
break;
|
|
case ContextProvider:
|
|
// Pop provider fiber
|
|
popProvider(workInProgress);
|
|
break;
|
|
case ContextConsumer:
|
|
break;
|
|
case MemoComponent:
|
|
break;
|
|
case IncompleteClassComponent: {
|
|
// Same as class component case. I put it down here so that the tags are
|
|
// sequential to ensure this switch is compiled to a jump table.
|
|
const Component = workInProgress.type;
|
|
if (isLegacyContextProvider(Component)) {
|
|
popLegacyContext(workInProgress);
|
|
}
|
|
break;
|
|
}
|
|
case DehydratedSuspenseComponent: {
|
|
if (enableSuspenseServerRenderer) {
|
|
popSuspenseContext(workInProgress);
|
|
if (current === null) {
|
|
let wasHydrated = popHydrationState(workInProgress);
|
|
invariant(
|
|
wasHydrated,
|
|
'A dehydrated suspense component was completed without a hydrated node. ' +
|
|
'This is probably a bug in React.',
|
|
);
|
|
skipPastDehydratedSuspenseInstance(workInProgress);
|
|
} else {
|
|
// We should never have been in a hydration state if we didn't have a current.
|
|
// However, in some of those paths, we might have reentered a hydration state
|
|
// and then we might be inside a hydration state. In that case, we'll need to
|
|
// exit out of it.
|
|
resetHydrationState();
|
|
if ((workInProgress.effectTag & DidCapture) === NoEffect) {
|
|
// This boundary did not suspend so it's now hydrated.
|
|
// To handle any future suspense cases, we're going to now upgrade it
|
|
// to a Suspense component. We detach it from the existing current fiber.
|
|
current.alternate = null;
|
|
workInProgress.alternate = null;
|
|
workInProgress.tag = SuspenseComponent;
|
|
workInProgress.memoizedState = null;
|
|
workInProgress.stateNode = null;
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
case SuspenseListComponent: {
|
|
popSuspenseContext(workInProgress);
|
|
|
|
const renderState: null | SuspenseListRenderState =
|
|
workInProgress.memoizedState;
|
|
|
|
if (renderState === null) {
|
|
// We're running in the default, "independent" mode. We don't do anything
|
|
// in this mode.
|
|
break;
|
|
}
|
|
|
|
let didSuspendAlready =
|
|
(workInProgress.effectTag & DidCapture) !== NoEffect;
|
|
|
|
let renderedTail = renderState.rendering;
|
|
if (renderedTail === null) {
|
|
// We just rendered the head.
|
|
if (!didSuspendAlready) {
|
|
// This is the first pass. We need to figure out if anything is still
|
|
// suspended in the rendered set.
|
|
|
|
// If new content unsuspended, but there's still some content that
|
|
// didn't. Then we need to do a second pass that forces everything
|
|
// to keep showing their fallbacks.
|
|
|
|
// We might be suspended if something in this render pass suspended, or
|
|
// something in the previous committed pass suspended. Otherwise,
|
|
// there's no chance so we can skip the expensive call to
|
|
// findFirstSuspended.
|
|
let cannotBeSuspended =
|
|
renderHasNotSuspendedYet() &&
|
|
(current === null || (current.effectTag & DidCapture) === NoEffect);
|
|
if (!cannotBeSuspended) {
|
|
let row = workInProgress.child;
|
|
while (row !== null) {
|
|
let suspended = findFirstSuspended(row);
|
|
if (suspended !== null) {
|
|
didSuspendAlready = true;
|
|
workInProgress.effectTag |= DidCapture;
|
|
cutOffTailIfNeeded(renderState, false);
|
|
|
|
// If this is a newly suspended tree, it might not get committed as
|
|
// part of the second pass. In that case nothing will subscribe to
|
|
// its thennables. Instead, we'll transfer its thennables to the
|
|
// SuspenseList so that it can retry if they resolve.
|
|
// There might be multiple of these in the list but since we're
|
|
// going to wait for all of them anyway, it doesn't really matter
|
|
// which ones gets to ping. In theory we could get clever and keep
|
|
// track of how many dependencies remain but it gets tricky because
|
|
// in the meantime, we can add/remove/change items and dependencies.
|
|
// We might bail out of the loop before finding any but that
|
|
// doesn't matter since that means that the other boundaries that
|
|
// we did find already has their listeners attached.
|
|
let newThennables = suspended.updateQueue;
|
|
if (newThennables !== null) {
|
|
workInProgress.updateQueue = newThennables;
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
|
|
// Rerender the whole list, but this time, we'll force fallbacks
|
|
// to stay in place.
|
|
// Reset the effect list before doing the second pass since that's now invalid.
|
|
workInProgress.firstEffect = workInProgress.lastEffect = null;
|
|
// Reset the child fibers to their original state.
|
|
resetChildFibers(workInProgress, renderExpirationTime);
|
|
|
|
// Set up the Suspense Context to force suspense and immediately
|
|
// rerender the children.
|
|
pushSuspenseContext(
|
|
workInProgress,
|
|
setShallowSuspenseContext(
|
|
suspenseStackCursor.current,
|
|
ForceSuspenseFallback,
|
|
),
|
|
);
|
|
return workInProgress.child;
|
|
}
|
|
row = row.sibling;
|
|
}
|
|
}
|
|
} else {
|
|
cutOffTailIfNeeded(renderState, false);
|
|
}
|
|
// Next we're going to render the tail.
|
|
} else {
|
|
// Append the rendered row to the child list.
|
|
if (!didSuspendAlready) {
|
|
let suspended = findFirstSuspended(renderedTail);
|
|
if (suspended !== null) {
|
|
workInProgress.effectTag |= DidCapture;
|
|
didSuspendAlready = true;
|
|
cutOffTailIfNeeded(renderState, true);
|
|
// This might have been modified.
|
|
if (
|
|
renderState.tail === null &&
|
|
renderState.tailMode === 'hidden'
|
|
) {
|
|
// We need to delete the row we just rendered.
|
|
// Ensure we transfer the update queue to the parent.
|
|
let newThennables = suspended.updateQueue;
|
|
if (newThennables !== null) {
|
|
workInProgress.updateQueue = newThennables;
|
|
workInProgress.effectTag |= Update;
|
|
}
|
|
// Reset the effect list to what it w as before we rendered this
|
|
// child. The nested children have already appended themselves.
|
|
let lastEffect = (workInProgress.lastEffect =
|
|
renderState.lastEffect);
|
|
// Remove any effects that were appended after this point.
|
|
if (lastEffect !== null) {
|
|
lastEffect.nextEffect = null;
|
|
}
|
|
// We're done.
|
|
return null;
|
|
}
|
|
} else if (
|
|
now() > renderState.tailExpiration &&
|
|
renderExpirationTime > Never
|
|
) {
|
|
// We have now passed our CPU deadline and we'll just give up further
|
|
// attempts to render the main content and only render fallbacks.
|
|
// The assumption is that this is usually faster.
|
|
workInProgress.effectTag |= DidCapture;
|
|
didSuspendAlready = true;
|
|
|
|
cutOffTailIfNeeded(renderState, false);
|
|
|
|
// Since nothing actually suspended, there will nothing to ping this
|
|
// to get it started back up to attempt the next item. If we can show
|
|
// them, then they really have the same priority as this render.
|
|
// So we'll pick it back up the very next render pass once we've had
|
|
// an opportunity to yield for paint.
|
|
|
|
const nextPriority = renderExpirationTime - 1;
|
|
workInProgress.expirationTime = workInProgress.childExpirationTime = nextPriority;
|
|
if (enableSchedulerTracing) {
|
|
markSpawnedWork(nextPriority);
|
|
}
|
|
}
|
|
}
|
|
if (renderState.isBackwards) {
|
|
// The effect list of the backwards tail will have been added
|
|
// to the end. This breaks the guarantee that life-cycles fire in
|
|
// sibling order but that isn't a strong guarantee promised by React.
|
|
// Especially since these might also just pop in during future commits.
|
|
// Append to the beginning of the list.
|
|
renderedTail.sibling = workInProgress.child;
|
|
workInProgress.child = renderedTail;
|
|
} else {
|
|
let previousSibling = renderState.last;
|
|
if (previousSibling !== null) {
|
|
previousSibling.sibling = renderedTail;
|
|
} else {
|
|
workInProgress.child = renderedTail;
|
|
}
|
|
renderState.last = renderedTail;
|
|
}
|
|
}
|
|
|
|
if (renderState.tail !== null) {
|
|
// We still have tail rows to render.
|
|
if (renderState.tailExpiration === 0) {
|
|
// Heuristic for how long we're willing to spend rendering rows
|
|
// until we just give up and show what we have so far.
|
|
const TAIL_EXPIRATION_TIMEOUT_MS = 500;
|
|
renderState.tailExpiration = now() + TAIL_EXPIRATION_TIMEOUT_MS;
|
|
}
|
|
// Pop a row.
|
|
let next = renderState.tail;
|
|
renderState.rendering = next;
|
|
renderState.tail = next.sibling;
|
|
renderState.lastEffect = workInProgress.lastEffect;
|
|
next.sibling = null;
|
|
|
|
// Restore the context.
|
|
// TODO: We can probably just avoid popping it instead and only
|
|
// setting it the first time we go from not suspended to suspended.
|
|
let suspenseContext = suspenseStackCursor.current;
|
|
if (didSuspendAlready) {
|
|
suspenseContext = setShallowSuspenseContext(
|
|
suspenseContext,
|
|
ForceSuspenseFallback,
|
|
);
|
|
} else {
|
|
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
|
|
}
|
|
pushSuspenseContext(workInProgress, suspenseContext);
|
|
// Do a pass over the next row.
|
|
return next;
|
|
}
|
|
break;
|
|
}
|
|
case FundamentalComponent: {
|
|
if (enableFundamentalAPI) {
|
|
const fundamentalImpl = workInProgress.type.impl;
|
|
let fundamentalInstance: ReactFundamentalComponentInstance<
|
|
any,
|
|
any,
|
|
> | null =
|
|
workInProgress.stateNode;
|
|
|
|
if (fundamentalInstance === null) {
|
|
const getInitialState = fundamentalImpl.getInitialState;
|
|
let fundamentalState;
|
|
if (getInitialState !== undefined) {
|
|
fundamentalState = getInitialState(newProps);
|
|
}
|
|
fundamentalInstance = workInProgress.stateNode = createFundamentalStateInstance(
|
|
workInProgress,
|
|
newProps,
|
|
fundamentalImpl,
|
|
fundamentalState || {},
|
|
);
|
|
const instance = ((getFundamentalComponentInstance(
|
|
fundamentalInstance,
|
|
): any): Instance);
|
|
fundamentalInstance.instance = instance;
|
|
if (fundamentalImpl.reconcileChildren === false) {
|
|
return null;
|
|
}
|
|
appendAllChildren(instance, workInProgress, false, false);
|
|
mountFundamentalComponent(fundamentalInstance);
|
|
} else {
|
|
// We fire update in commit phase
|
|
const prevProps = fundamentalInstance.props;
|
|
fundamentalInstance.prevProps = prevProps;
|
|
fundamentalInstance.props = newProps;
|
|
fundamentalInstance.currentFiber = workInProgress;
|
|
if (supportsPersistence) {
|
|
const instance = cloneFundamentalInstance(fundamentalInstance);
|
|
fundamentalInstance.instance = instance;
|
|
appendAllChildren(instance, workInProgress, false, false);
|
|
}
|
|
const shouldUpdate = shouldUpdateFundamentalComponent(
|
|
fundamentalInstance,
|
|
);
|
|
if (shouldUpdate) {
|
|
markUpdate(workInProgress);
|
|
}
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
default:
|
|
invariant(
|
|
false,
|
|
'Unknown unit of work tag. This error is likely caused by a bug in ' +
|
|
'React. Please file an issue.',
|
|
);
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
function mountEventResponder(
|
|
responder: ReactEventResponder<any, any>,
|
|
responderProps: Object,
|
|
instance: Instance,
|
|
rootContainerInstance: Container,
|
|
fiber: Fiber,
|
|
respondersMap: Map<
|
|
ReactEventResponder<any, any>,
|
|
ReactEventResponderInstance<any, any>,
|
|
>,
|
|
) {
|
|
let responderState = emptyObject;
|
|
const getInitialState = responder.getInitialState;
|
|
if (getInitialState !== null) {
|
|
responderState = getInitialState(responderProps);
|
|
}
|
|
const responderInstance = createResponderInstance(
|
|
responder,
|
|
responderProps,
|
|
responderState,
|
|
instance,
|
|
fiber,
|
|
);
|
|
mountResponderInstance(
|
|
responder,
|
|
responderInstance,
|
|
responderProps,
|
|
responderState,
|
|
instance,
|
|
rootContainerInstance,
|
|
);
|
|
respondersMap.set(responder, responderInstance);
|
|
}
|
|
|
|
function updateEventListener(
|
|
listener: ReactEventResponderListener<any, any>,
|
|
fiber: Fiber,
|
|
visistedResponders: Set<ReactEventResponder<any, any>>,
|
|
respondersMap: Map<
|
|
ReactEventResponder<any, any>,
|
|
ReactEventResponderInstance<any, any>,
|
|
>,
|
|
instance: Instance,
|
|
rootContainerInstance: Container,
|
|
): void {
|
|
let responder;
|
|
let props;
|
|
|
|
if (listener) {
|
|
responder = listener.responder;
|
|
props = listener.props;
|
|
}
|
|
invariant(
|
|
responder && responder.$$typeof === REACT_RESPONDER_TYPE,
|
|
'An invalid value was used as an event listener. Expect one or many event ' +
|
|
'listeners created via React.unstable_useResponder().',
|
|
);
|
|
const listenerProps = ((props: any): Object);
|
|
if (visistedResponders.has(responder)) {
|
|
// show warning
|
|
if (__DEV__) {
|
|
warning(
|
|
false,
|
|
'Duplicate event responder "%s" found in event listeners. ' +
|
|
'Event listeners passed to elements cannot use the same event responder more than once.',
|
|
responder.displayName,
|
|
);
|
|
}
|
|
return;
|
|
}
|
|
visistedResponders.add(responder);
|
|
const responderInstance = respondersMap.get(responder);
|
|
|
|
if (responderInstance === undefined) {
|
|
// Mount
|
|
mountEventResponder(
|
|
responder,
|
|
listenerProps,
|
|
instance,
|
|
rootContainerInstance,
|
|
fiber,
|
|
respondersMap,
|
|
);
|
|
} else {
|
|
// Update
|
|
responderInstance.props = listenerProps;
|
|
responderInstance.fiber = fiber;
|
|
}
|
|
}
|
|
|
|
function updateEventListeners(
|
|
listeners: any,
|
|
instance: Instance,
|
|
rootContainerInstance: Container,
|
|
fiber: Fiber,
|
|
): void {
|
|
const visistedResponders = new Set();
|
|
let dependencies = fiber.dependencies;
|
|
if (listeners != null) {
|
|
if (dependencies === null) {
|
|
dependencies = fiber.dependencies = {
|
|
expirationTime: NoWork,
|
|
firstContext: null,
|
|
responders: new Map(),
|
|
};
|
|
}
|
|
let respondersMap = dependencies.responders;
|
|
if (respondersMap === null) {
|
|
respondersMap = new Map();
|
|
}
|
|
if (isArray(listeners)) {
|
|
for (let i = 0, length = listeners.length; i < length; i++) {
|
|
const listener = listeners[i];
|
|
updateEventListener(
|
|
listener,
|
|
fiber,
|
|
visistedResponders,
|
|
respondersMap,
|
|
instance,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
} else {
|
|
updateEventListener(
|
|
listeners,
|
|
fiber,
|
|
visistedResponders,
|
|
respondersMap,
|
|
instance,
|
|
rootContainerInstance,
|
|
);
|
|
}
|
|
}
|
|
if (dependencies !== null) {
|
|
const respondersMap = dependencies.responders;
|
|
if (respondersMap !== null) {
|
|
// Unmount
|
|
const mountedResponders = Array.from(respondersMap.keys());
|
|
for (let i = 0, length = mountedResponders.length; i < length; i++) {
|
|
const mountedResponder = mountedResponders[i];
|
|
if (!visistedResponders.has(mountedResponder)) {
|
|
const responderInstance = ((respondersMap.get(
|
|
mountedResponder,
|
|
): any): ReactEventResponderInstance<any, any>);
|
|
unmountResponderInstance(responderInstance);
|
|
respondersMap.delete(mountedResponder);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
export {completeWork};
|