如何实现优雅的删除动画

前言

在日常开发中,删除动画往往比入场动画难实现的多。而且在很多例子中,尽管你写了很多删除动画,但他可能都不会生效。

比如我们在使用弹窗组件时,如果你想保留弹窗的离场动画,那么你可能会写出这样的代码:

tsx 复制代码
<Modal open={open} onClose={onClose}>
  <p>some content...</p>
  <p>some content...</p>
  <p>some content...</p>
</Modal>

但是,这就默认弹窗会直接渲染,但是我们很多时候希望打开弹窗时才去渲染 Modal 组件,于是我们就有了以下代码

tsx 复制代码
{open && (
  <Modal open={open} onClose={onClose}>
    <p>some content...</p>
    <p>some content...</p>
    <p>some content...</p>
  </Modal>
)}

在这种情况下,弹窗打开时动画还可以正常显示,但是关闭时候动画就直接失效了。

原因也很简单,当你关闭弹窗时,React 会立即卸载Modal 组件,动画根本没有机会执行,因为执行动画的元素已经不存在了。

那么,有什么方法可以让我们把动画执行完再卸载组件吗?

motion 组件

在鼎鼎大名的动画库 motion (framer-motion) 中,有一个 AnimatePresence 组件, 通过使用 AnimatePresence 包裹一个或多个 motion 组件,可以让我们在动画执行完后再卸载组件。

那么,为什么使用 AnimatePresence 就可以实现这个效果呢?

原理

状态管理

typescript 复制代码
//接收到的子组件
const presentChildren = useMemo(() => onlyElements(children), [children])
const presentKeys = presentChildren.map(getChildKey)

// 记录哪些子元素已经完成退出动画
const exitComplete = useConstant(() => new Map<ComponentKey, boolean>())

// 实际要渲染的字组件(可能包含已经卸载的组件)
const [renderedChildren, setRenderedChildren] = useState(presentChildren)

AnimatePresence 组件内部维护了 presentChildrenrenderedChildren 两个状态,用于检测子组件的变化。

存在感知

当子组件发生变化时,会遍历 renderedChildren

如果某个子组件的 key 不在 presentKeys 中,说明该组件已经被卸载,此时会在 exitComplete Map 中将该组件标记为未完成退出动画。

反之,如果子组件的 key 存在于 presentKeys 中,则从 exitComplete 中删除该组件的记录。

typescript 复制代码
useIsomorphicLayoutEffect(() => {
  for (let i = 0; i < renderedChildren.length; i++) {
      const key = getChildKey(renderedChildren[i])

      if (!presentKeys.includes(key)) {
          if (exitComplete.get(key) !== true) {
              exitComplete.set(key, false)
          }
      } else {
          exitComplete.delete(key)
      }
  }
}, [renderedChildren, presentKeys.length, presentKeys.join("-")])

维持组件的渲染

在渲染时,即使组件已经卸载,AnimatePresence 会维持组件的渲染。

同时,它会为每个子组件创建一个 PresenceChild 包装组件。 通过向 PresenceChild 传递 isPresent 属性来标识组件的存在状态,并通过 onExitComplete 回调在离场动画结束后执行真正的卸载操作 onExit

typescript 复制代码
<>
  {renderedChildren.map((child) => {
    const key = getChildKey(child)
    const isPresent = presentKeys.includes(key)

    return (
        <PresenceChild
            key={key}
            isPresent={isPresent} // 告诉子组件它是否"存在"
            onExitComplete={isPresent ? undefined : onExit}
        >
            {child}
        </PresenceChild>
    )
  })}
</>

退出动画的协调

PresenceChild 组件中,会创建一个 context, 通过 context 告诉当真正执行动画的 motion 组件。

  1. 通过 usePresence hook 订阅 PresenceContext
  2. 根据 isPresent 的值决定执行哪个动画:
    • isPresent === true: 执行 animate 定义的动画
    • isPresent === false: 执行 exit 定义的动画
typescript 复制代码
const context = useMemo(
  (): PresenceContextProps => ({
      id,
      isPresent,
      onExitComplete: memoizedOnExitComplete,
      register: (childId: string) => {
          presenceChildren.set(childId, false)
          return () => presenceChildren.delete(childId)
      },
  }),
  // ...
)

最终卸载流程

motion 组件内部会监听 onExitComplete 回调,当动画结束后,会调用 onExit 方法,真正卸载组件。

typescript 复制代码
const onExit = () => {
  if (exitComplete.has(key)) {
      exitComplete.set(key, true)
  }

  let isEveryExitComplete = true
  exitComplete.forEach((isExitComplete) => {
      if (!isExitComplete) isEveryExitComplete = false
  })

  if (isEveryExitComplete) {
      // 所有退出动画完成,更新渲染状态
      setRenderedChildren(pendingPresentChildren.current)
      onExitComplete && onExitComplete()
  }
}

总结

AnimatePresence 的实现主要包含以下几个部分:

  1. 内部维护了一个 renderedChildren 状态,这样即使父组件已经移除了子组件的渲染,AnimatePresence 依然能保持对它的控制

  2. 通过 context 向下传递"存在状态",子组件可以据此执行相应的动画效果

  3. exitComplete Map 来记录每个退出动画的完成情况,等所有动画都完成了才真正移除组件

  4. 如果组件没有退出动画,就直接移除,不会有多余的等待时间

通过这种巧妙的设计,我们可以在组件卸载时保持流畅的过渡动画效果,让用户体验更加自然。

相关推荐
Excel_VBA表格จุ๊บ1 小时前
wps宏js接入AI功能和接入翻译功能
javascript·wps·js宏
豪宇刘2 小时前
JavaScript 延迟加载的方法
开发语言·javascript
顾尘眠3 小时前
http常用状态码(204,304, 404, 504,502)含义
前端
摇光933 小时前
js迭代器模式
开发语言·javascript·迭代器模式
未命名冀3 小时前
微服务面试相关
java·微服务·面试
王先生技术栈4 小时前
思维导图,Android版本实现
java·前端
悠悠:)5 小时前
前端 动图方案
前端
anyup_前端梦工厂5 小时前
了解 ES6 的变量特性:Var、Let、Const
开发语言·javascript·ecmascript
星陈~5 小时前
检测electron打包文件 app.asar
前端·vue.js·electron
Aatroox5 小时前
基于 Nuxt3 + Obsidian 搭建个人博客
前端·node.js