如何实现优雅的删除动画

前言

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

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

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

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

相关推荐
快乐非自愿3 分钟前
商品中心—库存分桶高并发的优化文档
java·前端·spring
灰海6 分钟前
原型与原型链到底是什么?
开发语言·前端·javascript·es6·原型模式·原生js
玲小珑8 分钟前
Next.js 教程系列(十四)NextAuth.js 身份认证与授权
前端·next.js
山河木马22 分钟前
前端学C++可太简单了:双冒号 :: 操作符
前端·javascript·c++
汪子熙23 分钟前
什么是 ArkTS
后端·面试
3Katrina23 分钟前
前端面试之防抖节流(二)
前端·javascript·面试
前端进阶者29 分钟前
天地图编辑支持删除编辑点
前端·javascript
江号软件分享38 分钟前
无接触服务的关键:二维码生成识别技术详解
前端
江号软件分享39 分钟前
如何利用取色器实现跨平台色彩一致性
前端
Z字小熊饼干爱吃保安39 分钟前
面试技术问题总结一
数据库·面试·职场和发展