如何实现优雅的删除动画

前言

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

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

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. 如果组件没有退出动画,就直接移除,不会有多余的等待时间

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

相关推荐
不简说26 分钟前
JS 代码技巧 vol.1 — 10 个让代码少写 30% 的小套路
前端·javascript·面试
@@@@@@@@5429 分钟前
JavaScript 的缺点与 TypeScript 的弥补
开发语言·javascript·typescript
Hilaku30 分钟前
为什么业务型前端容易遭遇中年危机?
前端·javascript·程序员
এ慕ོ冬℘゜39 分钟前
深度解析 JavaScript:赋予 Web 灵魂的“全栈语言”
开发语言·前端·javascript
iCOD3R41 分钟前
Skill - 3秒生成精美GitHub主页
前端·程序员·ai编程
Mr_凌宇44 分钟前
# AI 应用工程入门学习笔记 一
人工智能·面试·程序员
爱勇宝1 小时前
《道德经》第5章:生产环境不相信眼泪
前端·后端·架构
掘金者阿豪1 小时前
LEFT JOIN 凭空消失的背后,是优化器偷偷帮你做了决定
前端·后端
HackTwoHub1 小时前
AntiDebug Mcp、AI逆向绕过前端,Hook 加密接口,抓取 Vue 路由漏洞,接入 MCP 让 AI 自动化挖掘前端漏洞
前端·vue.js·人工智能·安全·web安全·网络安全·系统安全
Mr_凌宇1 小时前
AI 应用工程进阶扩展学习笔记 二
前端·面试