跳到主要内容

React Fiber 提交视图过渡

一、作用

二、导出的变量

1. 应该开始视图过渡

export let shouldStartViewTransition: boolean = false;

2. 出现视图过渡

// This tracks named ViewTransition components found in the accumulateSuspenseyCommit
// phase that might need to find deleted pairs in the beforeMutation phase.
//
// 这会跟踪在 accumulateSuspenseyCommit 阶段发现的命名 ViewTransition 组件,这些组件可能
// 需要在 beforeMutation 阶段查找已删除的配对。
export let appearingViewTransitions: Map<string, ViewTransitionState> | null =
null;

3. 可取消视图过渡的子项

// We can't cancel view transition children until we know that their parent also
// don't need to transition.
//
// 我们不能取消视图过渡的子元素,直到我们确定它们的父元素也不需要过渡。
export let viewTransitionCancelableChildren: null | Array<
Instance | string | Props
> = null; // tupled array where each entry is [instance: Instance, oldName: string, props: Props]
// 元组数组,每个条目都是 [instance: 实例, oldName: 字符串, props: 属性]

三、重置应开始视图过渡

export function resetShouldStartViewTransition(): void {
shouldStartViewTransition = false;
}

四、重置出现视图过渡

export function resetAppearingViewTransitions(): void {
appearingViewTransitions = null;
}

五、跟踪视图出现过渡

export function trackAppearingViewTransition(
name: string,
state: ViewTransitionState,
): void {
if (appearingViewTransitions === null) {
appearingViewTransitions = new Map();
}
appearingViewTransitions.set(name, state);
}

六、跟踪进入视图的过渡

export function trackEnterViewTransitions(placement: Fiber): void {
if (
placement.tag === ViewTransitionComponent ||
(placement.subtreeFlags & ViewTransitionStatic) !== NoFlags
) {
// If an inserted or appearing Fiber is a ViewTransition component or has one as
// an immediate child, then that will trigger as an "Enter" in future passes.
// We don't do anything else for that case in the "before mutation" phase but we
// still have to mark it as needing to call startViewTransition if nothing else
// updates.
//
// 如果插入的或出现的 Fiber 是一个 ViewTransition 组件,或者它的直接子组件是 ViewTransition 组件,
// 那么在未来的渲染过程中这将触发一次“进入”操作。
// 在“变更之前”阶段我们不会对这种情况做其他处理,但如果没有其他更新,
// 我们仍然必须将其标记为需要调用 startViewTransition。
shouldStartViewTransition = true;
}
}

七、可取消视图转换范围

export function pushViewTransitionCancelableScope(): null | Array<
Instance | string | Props
> {
const prevChildren = viewTransitionCancelableChildren;
viewTransitionCancelableChildren = null;
return prevChildren;
}

八、可取消视图弹出过渡范围

export function popViewTransitionCancelableScope(
prevChildren: null | Array<Instance | string | Props>,
): void {
viewTransitionCancelableChildren = prevChildren;
}

九、提交进入视图过渡

备注
export function commitEnterViewTransitions(
placement: Fiber,
gesture: boolean,
): void {
if (placement.tag === ViewTransitionComponent) {
const state: ViewTransitionState = placement.stateNode;
const props: ViewTransitionProps = placement.memoizedProps;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.default,
state.paired ? props.share : props.enter,
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
placement,
name,
className,
null,
false,
);
if (!inViewport) {
// TODO: If this was part of a pair we will still run the onShare callback.
// Revert the transition names. This boundary is not in the viewport
// so we won't bother animating it.
// 待办事项:如果这是成对的一部分,我们仍然会运行 onShare 回调。
// 恢复过渡名称。这个边界不在视口中,所以我们不会去处理动画。
restoreViewTransitionOnHostInstances(placement.child, false);
// TODO: Should we still visit the children in case a named one was in the viewport?
// 待办:如果一个命名的子元素在视口中,我们是否仍然应该访问其子元素?
} else {
commitAppearingPairViewTransitions(placement);

if (!state.paired) {
if (gesture) {
// TODO: Schedule gesture events.
// 待办:安排手势事件。
} else {
scheduleViewTransitionEvent(placement, props.onEnter);
}
}
}
} else {
commitAppearingPairViewTransitions(placement);
}
} else if ((placement.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = placement.child;
while (child !== null) {
commitEnterViewTransitions(child, gesture);
child = child.sibling;
}
} else {
commitAppearingPairViewTransitions(placement);
}
}

十、提交退出视图过渡

备注
export function commitExitViewTransitions(deletion: Fiber): void {
if (deletion.tag === ViewTransitionComponent) {
const props: ViewTransitionProps = deletion.memoizedProps;
const name = getViewTransitionName(props, deletion.stateNode);
const pair =
appearingViewTransitions !== null
? appearingViewTransitions.get(name)
: undefined;
const className: ?string = getViewTransitionClassName(
props.default,
pair !== undefined ? props.share : props.exit,
);
if (className !== 'none') {
const inViewport = applyViewTransitionToHostInstances(
deletion,
name,
className,
null,
false,
);
if (!inViewport) {
// Revert the transition names. This boundary is not in the viewport
// so we won't bother animating it.
//
// 恢复过渡名称。此边界不在视口内,所以我们不会费心去动画它。
restoreViewTransitionOnHostInstances(deletion.child, false);
// TODO: Should we still visit the children in case a named one was in the viewport?
// 待办事项:如果某个命名的子元素在视口中,我们是否仍然应该访问这些子元素?
} else if (pair !== undefined) {
// We found a new appearing view transition with the same name as this deletion.
// We'll transition between them instead of running the normal exit.
//
// 我们发现了一个新出现的视图转换,其名称与此删除操作相同。
// 我们将切换它们之间的过渡,而不是执行正常的退出操作。
const oldInstance: ViewTransitionState = deletion.stateNode;
const newInstance: ViewTransitionState = pair;
newInstance.paired = oldInstance;
oldInstance.paired = newInstance;
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
// $FlowFixMe[incompatible-use]: Refined by the pair.
//
// 删除该条目,以便我们知道何时找到了所有条目
// 并且可以停止搜索(大小达到零)。
// $FlowFixMe[incompatible-use]:已通过配对进行优化。
appearingViewTransitions.delete(name);
// Note: If the other side ends up outside the viewport, we'll still run this.
// Therefore it's possible for onShare to be called with only an old snapshot.
//
// 注意:如果另一方最终位于视口之外,我们仍然会运行这个。
// 因此有可能 onShare 会仅使用旧快照被调用。
scheduleViewTransitionEvent(deletion, props.onShare);
} else {
scheduleViewTransitionEvent(deletion, props.onExit);
}
}
if (appearingViewTransitions !== null) {
// Look for more pairs deeper in the tree.
// 在树的更深层查找更多的键值对。
commitDeletedPairViewTransitions(deletion);
}
} else if ((deletion.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = deletion.child;
while (child !== null) {
commitExitViewTransitions(child);
child = child.sibling;
}
} else {
if (appearingViewTransitions !== null) {
commitDeletedPairViewTransitions(deletion);
}
}
}

十一、在更新视图过渡之前提交

备注
export function commitBeforeUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
): void {
// The way we deal with multiple HostInstances as children of a View Transition in an
// update can get tricky. The important bit is that if you swap out n HostInstances
// from n HostInstances then they match up in order. Similarly, if you don't swap
// any HostInstances each instance just transitions as is.
//
// 在更新中,处理作为视图过渡子项的多个 HostInstances 可能会变得棘手。关键在于,如果你
// 用 n 个 HostInstances 替换 n 个 HostInstances,它们的顺序会匹配。类似地,如果你
// 不替换任何 HostInstances,每个实例将按原样过渡。
//
// We call this function twice. First we apply the view transition names on the
// "current" tree in the snapshot phase. Then in the mutation phase we apply view
// transition names to the "finishedWork" tree.
//
// 我们调用这个函数两次。第一次在快照阶段将视图过渡名称应用到“当前”树上。然后在变更阶段,我们将
// 视图过渡名称应用到“finishedWork”树上。
//
// This means that if there were insertions or deletions before an updated Instance
// that same Instance might get different names in the "old" and the "new" state.
// For example if you swap two HostInstances inside a ViewTransition they don't
// animate to swap position but rather cross-fade into the other instance. This might
// be unexpected but it is in line with the semantics that the ViewTransition is its
// own layer that cross-fades its content when it updates. If you want to reorder then
// each child needs its own ViewTransition.
//
// 这意味着,如果在更新的实例之前有插入或删除操作,
// 那么同一个实例在“旧”状态和“新”状态下可能会有不同的名称。
// 例如,如果你在 ViewTransition 内交换两个 HostInstances,
// 它们不会以交换位置的方式进行动画,而是会相互淡入淡出到另一实例。
// 这可能会让人感到意外,但这是符合 ViewTransition 语义的——
// 当它更新时,ViewTransition 是它自己的层,会对内容进行淡入淡出。
// 如果你想重新排序,那么每个子项都需要它自己的 ViewTransition。
const oldProps: ViewTransitionProps = current.memoizedProps;
const oldName = getViewTransitionName(oldProps, current.stateNode);
const newProps: ViewTransitionProps = finishedWork.memoizedProps;
// This className applies only if there are fewer child DOM nodes than
// before or if this update should've been cancelled but we ended up with
// a parent animating so we need to animate the child too.
// For example, if update="foo" layout="none" and it turns out this was
// a layout only change, then the "foo" class will be applied even though
// it was not actually an update. Which is a bug.
//
// 该 className 仅在子 DOM 节点少于之前的数量,
// 或者如果该更新本应被取消但最终父元素正在执行动画时应用,
// 因为我们也需要对子元素进行动画。
// 例如,如果 update="foo" layout="none" 并且事实证明这只是布局变更,
// 那么 "foo" 类仍会被应用,尽管它实际上并不是一次更新。这是一个 bug。
const className: ?string = getViewTransitionClassName(
newProps.default,
newProps.update,
);
if (className === 'none') {
// If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
// 如果更新为“none”,那么我们不必应用名称。因为我们不会对这个边界进行动画处理。
return;
}
applyViewTransitionToHostInstances(
current,
oldName,
className,
(current.memoizedState = []),
true,
);
}

十二、提交嵌套视图过渡

备注
export function commitNestedViewTransitions(changedParent: Fiber): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
// In this case the outer ViewTransition component wins but if there
// was an update through this component then the inner one wins.
//
// 在这种情况下,外部的 ViewTransition 组件胜出,但如果通过该组件进行了
// 更新,则内部的组件会胜出。
const props: ViewTransitionProps = child.memoizedProps;
const name = getViewTransitionName(props, child.stateNode);
const className: ?string = getViewTransitionClassName(
props.default,
props.update,
);
// "Nested" view transitions are in subtrees that didn't update so
// this is a "current". We normally clear this upon rerendering
// but we use this flag to track changes from layout in the commit.
// So we need it to be cleared before we do that.
// TODO: Use some other temporary state to track this.
//
// “嵌套”视图过渡位于未更新的子树中,所以这是一个“当前”状态。我们通常会在重新渲染
// 时清除它,但我们使用这个标记来跟踪提交阶段布局的变化。因此,在我们执行该操作之前,需要清除它。
// TODO: 使用其他临时状态来跟踪这一点。
child.flags &= ~Update;
if (className !== 'none') {
applyViewTransitionToHostInstances(
child,
name,
className,
(child.memoizedState = []),
false,
);
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
commitNestedViewTransitions(child);
}
child = child.sibling;
}
}

十三、恢复进入或退出视图的过渡

export function restoreEnterOrExitViewTransitions(fiber: Fiber): void {
if (fiber.tag === ViewTransitionComponent) {
const instance: ViewTransitionState = fiber.stateNode;
instance.paired = null;
restoreViewTransitionOnHostInstances(fiber.child, false);
restorePairedViewTransitions(fiber);
} else if ((fiber.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
let child = fiber.child;
while (child !== null) {
restoreEnterOrExitViewTransitions(child);
child = child.sibling;
}
} else {
restorePairedViewTransitions(fiber);
}
}

十四、恢复更新视图过渡

export function restoreUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
): void {
restoreViewTransitionOnHostInstances(current.child, true);
restoreViewTransitionOnHostInstances(finishedWork.child, true);
}

十五、为手势恢复更新视图过渡

export function restoreUpdateViewTransitionForGesture(
current: Fiber,
finishedWork: Fiber,
): void {
// For gestures we don't need to reset "finishedWork" because those would
// have all been clones that got deleted.
//
// 对于手势,我们不需要重置“finishedWork”,因为那些都是被删除的克隆。
restoreViewTransitionOnHostInstances(current.child, true);
}

十六、恢复嵌套视图过渡

export function restoreNestedViewTransitions(changedParent: Fiber): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
restoreViewTransitionOnHostInstances(child.child, false);
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
restoreNestedViewTransitions(child);
}
child = child.sibling;
}
}

十七、测量视图过渡宿主环境实例

export function measureViewTransitionHostInstances(
parentViewTransition: Fiber,
child: null | Fiber,
newName: string,
oldName: string,
className: ?string,
previousMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
viewTransitionHostInstanceIdx = 0;
return measureViewTransitionHostInstancesRecursive(
parentViewTransition,
child,
newName,
oldName,
className,
previousMeasurements,
stopAtNestedViewTransitions,
);
}

十八、测量更新视图过渡

备注
export function measureUpdateViewTransition(
current: Fiber,
finishedWork: Fiber,
gesture: boolean,
): boolean {
// If this was a gesture then which Fiber was used for the "old" vs "new" state is reversed.
// We still need to treat "finishedWork" as the Fiber that contains the flags for this commmit.
// 如果这是一个手势,那么“旧状态”和“新状态”使用的 Fiber 是反过来的。
// 我们仍然需要将“finishedWork”视为包含此提交标记的 Fiber。
const oldFiber = gesture ? finishedWork : current;
const newFiber = gesture ? current : finishedWork;
const props: ViewTransitionProps = newFiber.memoizedProps;
const state: ViewTransitionState = newFiber.stateNode;
const newName = getViewTransitionName(props, state);
const oldName = getViewTransitionName(oldFiber.memoizedProps, state);
// Whether it ends up having been updated or relayout we apply the update class name.
// 无论最终是更新还是重新布局,我们都会应用更新的类名。
const className: ?string = getViewTransitionClassName(
props.default,
props.update,
);
if (className === 'none') {
// If update is "none" then we don't have to apply a name. Since we won't animate this boundary.
// 如果更新为“none”,那么我们不必应用名称。因为我们不会对这个边界进行动画处理。
return false;
}
// If nothing changed due to a mutation, or children changing size
// and the measurements end up unchanged, we should restore it to not animate.
//
// 如果因为某个变动没有发生变化,或者子元素改变了大小
// 且测量结果保持不变,我们应该将其恢复为不动画状态。
let previousMeasurements: null | Array<InstanceMeasurement>;
if (gesture) {
const clones = state.clones;
if (clones === null) {
previousMeasurements = null;
} else {
previousMeasurements = clones.map(measureClonedInstance);
}
} else {
previousMeasurements = oldFiber.memoizedState;
// Clear it. We won't need it anymore.
oldFiber.memoizedState = null; // Clear it. We won't need it anymore.
}
const inViewport = measureViewTransitionHostInstances(
// 这始终是 finishedWork,因为它用于分配标志。
finishedWork, // This is always finishedWork since it's used to assign flags.
// 这取决于是否是手势而是当前的工作还是已完成的工作。
newFiber.child, // This either current or finishedWork depending on if was a gesture.
newName,
oldName,
className,
previousMeasurements,
true,
);
const previousCount =
previousMeasurements === null ? 0 : previousMeasurements.length;
if (viewTransitionHostInstanceIdx !== previousCount) {
// If we found a different number of child DOM nodes we need to assume that
// the parent layout may have changed as a result. This is not necessarily
// true if those nodes were absolutely positioned.
//
// 如果我们发现子 DOM 节点的数量不同,则需要假设父布局可能已发生变化。
// 如果这些节点是绝对定位的,则不一定如此。
finishedWork.flags |= AffectedParentLayout;
}
return inViewport;
}

十九、测量嵌套视图过渡

备注
export function measureNestedViewTransitions(
changedParent: Fiber,
gesture: boolean,
): void {
let child = changedParent.child;
while (child !== null) {
if (child.tag === ViewTransitionComponent) {
const props: ViewTransitionProps = child.memoizedProps;
const state: ViewTransitionState = child.stateNode;
const name = getViewTransitionName(props, state);
const className: ?string = getViewTransitionClassName(
props.default,
props.update,
);
let previousMeasurements: null | Array<InstanceMeasurement>;
if (gesture) {
const clones = state.clones;
if (clones === null) {
previousMeasurements = null;
} else {
previousMeasurements = clones.map(measureClonedInstance);
}
} else {
previousMeasurements = child.memoizedState;
// 清理它。我们不再需要它了。
child.memoizedState = null; // Clear it. We won't need it anymore.
}
const inViewport = measureViewTransitionHostInstances(
child,
child.child,
name,
// 由于没有更改,新旧名称相同。
name, // Since this is unchanged, new and old name is the same.
className,
previousMeasurements,
false,
);
if ((child.flags & Update) === NoFlags || !inViewport) {
// Nothing changed.
// 无事退朝
} else {
if (gesture) {
// TODO: Schedule gesture events.
// 待办:安排手势事件。
} else {
scheduleViewTransitionEvent(child, props.onUpdate);
}
}
} else if ((child.subtreeFlags & ViewTransitionStatic) !== NoFlags) {
measureNestedViewTransitions(child, gesture);
}
child = child.sibling;
}
}

二十、变量

// 视图过渡宿主实例索引
let viewTransitionHostInstanceIdx = 0;

廿一、工具

1. 将视图过渡应用于宿主实例

备注
function applyViewTransitionToHostInstances(
fiber: Fiber,
name: string,
className: ?string,
collectMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
viewTransitionHostInstanceIdx = 0;
const inViewport = applyViewTransitionToHostInstancesRecursive(
fiber.child,
name,
className,
collectMeasurements,
stopAtNestedViewTransitions,
);
if (enableProfilerTimer && enableComponentPerformanceTrack && inViewport) {
if (fiber._debugTask != null) {
trackAnimatingTask(fiber._debugTask);
}
}
return inViewport;
}

2. 递归应用视图过渡到宿主环境实例

备注
  • measureInstance() 由宿主环境提供
  • wasInstanceInViewport() 由宿主环境提供
  • applyViewTransitionName() 由宿主环境提供
function applyViewTransitionToHostInstancesRecursive(
child: null | Fiber,
name: string,
className: ?string,
collectMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
if (!supportsMutation) {
return false;
}
let inViewport = false;
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
if (collectMeasurements !== null) {
const measurement = measureInstance(instance);
collectMeasurements.push(measurement);
if (wasInstanceInViewport(measurement)) {
inViewport = true;
}
} else if (!inViewport) {
if (wasInstanceInViewport(measureInstance(instance))) {
inViewport = true;
}
}
shouldStartViewTransition = true;
applyViewTransitionName(
instance,
viewTransitionHostInstanceIdx === 0
? name
: // If we have multiple Host Instances below, we add a suffix to the name to give
// each one a unique name.
name + '_' + viewTransitionHostInstanceIdx,
className,
);
viewTransitionHostInstanceIdx++;
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
// Skip any hidden subtrees. They were or are effectively not there.
// 跳过任何隐藏的子树。它们实际上从未存在或已经不存在。
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
// Skip any nested view transitions for updates since in that case the
// inner most one is the one that handles the update.
//
// 对于更新,跳过任何嵌套视图过渡,因为在这种情况下,最内层的视图处理更新。
} else {
if (
applyViewTransitionToHostInstancesRecursive(
child.child,
name,
className,
collectMeasurements,
stopAtNestedViewTransitions,
)
) {
inViewport = true;
}
}
child = child.sibling;
}
return inViewport;
}

3. 在宿主环境实例上恢复视图过渡

备注
  • restoreViewTransitionName() 由宿主环境提供
function restoreViewTransitionOnHostInstances(
child: null | Fiber,
stopAtNestedViewTransitions: boolean,
): void {
if (!supportsMutation) {
return;
}
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
restoreViewTransitionName(instance, child.memoizedProps);
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
// Skip any hidden subtrees. They were or are effectively not there.
// 跳过任何隐藏的子树。它们实际上从未存在或已经不存在。
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
// Skip any nested view transitions for updates since in that case the
// inner most one is the one that handles the update.
//
// 对于更新,跳过任何嵌套的视图过渡,因为在这种情况下,最内层的视图处理更新。
} else {
restoreViewTransitionOnHostInstances(
child.child,
stopAtNestedViewTransitions,
);
}
child = child.sibling;
}
}

4. 提交出现的配对视图过渡

备注
function commitAppearingPairViewTransitions(placement: Fiber): void {
if ((placement.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
// This has no named view transitions in its subtree.
// 其子树中没有命名的视图过渡。
return;
}
let child = placement.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
// This tree was already hidden so we skip it.
// 这棵树已经被隐藏,所以我们跳过它。
} else {
commitAppearingPairViewTransitions(child);
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const instance: ViewTransitionState = child.stateNode;
if (instance.paired) {
const props: ViewTransitionProps = child.memoizedProps;
if (props.name == null || props.name === 'auto') {
throw new Error(
'Found a pair with an auto name. This is a bug in React.',
);
}
const name = props.name;
const className: ?string = getViewTransitionClassName(
props.default,
props.share,
);
if (className !== 'none') {
// We found a new appearing view transition with the same name as this deletion.
// We'll transition between them.
//
// 我们发现了一个新出现的视图转换,它与此删除操作的名称相同。
// 我们将在它们之间进行转换。
const inViewport = applyViewTransitionToHostInstances(
child,
name,
className,
null,
false,
);
if (!inViewport) {
// This boundary is exiting within the viewport but is going to leave the viewport.
// Instead, we treat this as an exit of the previous entry by reverting the new name.
// Ideally we could undo the old transition but it's now too late. It's also on its
// on snapshot. We have know was for it to paint onto the original group.
// TODO: This will lead to things unexpectedly having exit animations that normally
// wouldn't happen. Consider if we should just let this fly off the screen instead.
//
// 此边界正在视口内退出,但将要离开视口。
// 相反,我们将其视为上一个条目的退出,通过恢复新名称来处理。
// 理想情况下,我们可以撤销旧的过渡,但现在已为时过晚。它也在自己的
// 快照上。我们必须让它绘制到原始组中。
// 待办事项:这可能会导致本不应发生的退出动画意外出现。考虑是否应该让它直接飞出屏幕。
restoreViewTransitionOnHostInstances(child.child, false);
}
}
}
}
}
child = child.sibling;
}
}

5. 提交已删除配对视图过渡

备注
function commitDeletedPairViewTransitions(deletion: Fiber): void {
if (
appearingViewTransitions === null ||
appearingViewTransitions.size === 0
) {
// We've found all.
// 我们已经找到了全部。
return;
}
const pairs = appearingViewTransitions;
if ((deletion.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
// This has no named view transitions in its subtree.
// 其子树中没有命名的视图过渡。// 其子树中没有命名的视图过渡。
return;
}
let child = deletion.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
// This tree was already hidden so we skip it.
// 这棵树已经被隐藏,所以我们跳过它。
} else {
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const props: ViewTransitionProps = child.memoizedProps;
const name = props.name;
if (name != null && name !== 'auto') {
const pair = pairs.get(name);
if (pair !== undefined) {
const className: ?string = getViewTransitionClassName(
props.default,
props.share,
);
if (className !== 'none') {
// We found a new appearing view transition with the same name as this deletion.
// 我们发现了一个与此删除同名的新出现视图过渡。
const inViewport = applyViewTransitionToHostInstances(
child,
name,
className,
null,
false,
);
if (!inViewport) {
// This boundary is not in the viewport so we won't treat it as a matched pair.
// Revert the transition names. This avoids it flying onto the screen which can
// be disruptive and doesn't really preserve any continuity anyway.
//
// 这个边界不在视口内,所以我们不会将其视为匹配对。
// 恢复过渡名称。这可以避免它突然出现在屏幕上,
// 这种情况可能会造成干扰,而且实际上也无法保留任何连续性。
restoreViewTransitionOnHostInstances(child.child, false);
} else {
// We'll transition between them.
// 我们将在它们之间过渡。
const oldInstance: ViewTransitionState = child.stateNode;
const newInstance: ViewTransitionState = pair;
newInstance.paired = oldInstance;
oldInstance.paired = newInstance;
// Note: If the other side ends up outside the viewport, we'll still run this.
// Therefore it's possible for onShare to be called with only an old snapshot.
//
// 注意:如果另一方最终位于视口之外,我们仍然会运行这个。
// 因此有可能 onShare 会仅使用旧的快照被调用。
scheduleViewTransitionEvent(child, props.onShare);
}
}
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
//
// Delete the entry so that we know when we've found all of them
// and can stop searching (size reaches zero).
pairs.delete(name);
if (pairs.size === 0) {
break;
}
}
}
}
commitDeletedPairViewTransitions(child);
}
child = child.sibling;
}
}

6. 恢复配对视图过渡

function restorePairedViewTransitions(parent: Fiber): void {
if ((parent.subtreeFlags & ViewTransitionNamedStatic) === NoFlags) {
// This has no named view transitions in its subtree.
// 其子树中没有命名的视图过渡。
return;
}
let child = parent.child;
while (child !== null) {
if (child.tag === OffscreenComponent && child.memoizedState !== null) {
// This tree was already hidden so we skip it.
// 这棵树已经被隐藏,所以我们跳过它。
} else {
if (
child.tag === ViewTransitionComponent &&
(child.flags & ViewTransitionNamedStatic) !== NoFlags
) {
const instance: ViewTransitionState = child.stateNode;
if (instance.paired !== null) {
instance.paired = null;
restoreViewTransitionOnHostInstances(child.child, false);
}
}
restorePairedViewTransitions(child);
}
child = child.sibling;
}
}

7. 递归测量视图过渡宿主实例

备注
  • measureInstance() 由宿主环境提供
  • wasInstanceInViewport() 由宿主环境提供
  • hasInstanceChanged() 由宿主环境提供
  • hasInstanceAffectedParent() 由宿主环境提供
  • applyViewTransitionName() 由宿主环境提供
function measureViewTransitionHostInstancesRecursive(
parentViewTransition: Fiber,
child: null | Fiber,
newName: string,
oldName: string,
className: ?string,
previousMeasurements: null | Array<InstanceMeasurement>,
stopAtNestedViewTransitions: boolean,
): boolean {
if (!supportsMutation) {
return true;
}
let inViewport = false;
while (child !== null) {
if (child.tag === HostComponent) {
const instance: Instance = child.stateNode;
if (
previousMeasurements !== null &&
viewTransitionHostInstanceIdx < previousMeasurements.length
) {
// The previous measurement of the Instance in this location within the ViewTransition.
// Note that this might not be the same exact Instance if the Instances within the
// ViewTransition changed.
//
// 之前在这个 ViewTransition 中此位置对实例的测量。
// 请注意,如果 ViewTransition 中的实例发生了变化,这可能不是完全相同的实例。
const previousMeasurement =
previousMeasurements[viewTransitionHostInstanceIdx];
const nextMeasurement = measureInstance(instance);
if (
wasInstanceInViewport(previousMeasurement) ||
wasInstanceInViewport(nextMeasurement)
) {
// If either the old or new state was within the viewport we have to animate this.
// But if it turns out that none of them were we'll be able to skip it.
//
// 如果旧状态或新状态在视口内,我们必须进行动画处理。
// 但如果结果发现它们都不在,我们就可以跳过这一步。
inViewport = true;
}
if (
(parentViewTransition.flags & Update) === NoFlags &&
hasInstanceChanged(previousMeasurement, nextMeasurement)
) {
parentViewTransition.flags |= Update;
}
if (hasInstanceAffectedParent(previousMeasurement, nextMeasurement)) {
// If this instance size within its parent has changed it might have caused the
// parent to relayout which needs a cross fade.
//
// 如果这个实例在其父级中的大小发生了变化,可能会导致父级重新布局,需要交叉淡入淡出。
parentViewTransition.flags |= AffectedParentLayout;
}
} else {
// If there was an insertion of extra nodes, we have to assume they affected the parent.
// It should have already been marked as an Update due to the mutation.
//
// 如果插入了额外的节点,我们必须假设它们影响了父节点。
// 由于变更,它应该已经被标记为更新。
parentViewTransition.flags |= AffectedParentLayout;
}
if ((parentViewTransition.flags & Update) !== NoFlags) {
// We might update this node so we need to apply its new name for the new state.
// Additionally in the ApplyGesture case we also need to do this because the clone
// will have the name but this one won't.
//
// 我们可能会更新这个节点,所以我们需要为新状态应用它的新名称。
// 同样,在 ApplyGesture 的情况下我们也需要这样做,因为克隆会有名称,但这个不会。
applyViewTransitionName(
instance,
viewTransitionHostInstanceIdx === 0
? newName
: // If we have multiple Host Instances below, we add a suffix to the name to give
// each one a unique name.
//
// 如果下面有多个宿主环境实例,我们会在名称后添加一个后缀以给每个实例一个唯一的名称。
newName + '_' + viewTransitionHostInstanceIdx,
className,
);
}
if (!inViewport || (parentViewTransition.flags & Update) === NoFlags) {
// It turns out that we had no other deeper mutations, the child transitions didn't
// affect the parent layout and this instance hasn't changed size. So we can skip
// animating it. However, in the current model this only works if the parent also
// doesn't animate. So we have to queue these and wait until we complete the parent
// to cancel them.
//
// 结果表明,我们没有其他更深层的变动,子元素的变化没有影响父布局,而且这个实例的大小没有改变。因此
// 我们可以跳过对它的动画。不过,在当前模型中,这只在父元素也不进行动画时才可行。所以我们必须将这些
// 操作排队,等待父元素完成后再取消它们。
if (viewTransitionCancelableChildren === null) {
viewTransitionCancelableChildren = [];
}
viewTransitionCancelableChildren.push(
instance,
oldName,
child.memoizedProps,
);
}
viewTransitionHostInstanceIdx++;
} else if (
child.tag === OffscreenComponent &&
child.memoizedState !== null
) {
// Skip any hidden subtrees. They were or are effectively not there.
// 跳过任何隐藏的子树。它们实际上从未存在或已经不存在。
} else if (
child.tag === ViewTransitionComponent &&
stopAtNestedViewTransitions
) {
// Skip any nested view transitions for updates since in that case the
// inner most one is the one that handles the update.
// If this inner boundary resized we need to bubble that information up.
//
// 跳过任何嵌套视图的更新过渡,因为在这种情况下,最内层的过渡会处理更新。
// 如果这个内层边界调整了大小,我们需要将该信息向上冒泡。
parentViewTransition.flags |= child.flags & AffectedParentLayout;
} else {
if (
measureViewTransitionHostInstancesRecursive(
parentViewTransition,
child.child,
newName,
oldName,
className,
previousMeasurements,
stopAtNestedViewTransitions,
)
) {
inViewport = true;
}
}
child = child.sibling;
}
return inViewport;
}