2025 年 1 月 13 日

揭秘:React 的实验性动画 API

马特·佩里

自从 12(!) 年前诞生以来,React 的 API 中一直存在一个明显的动画功能缺失。

多年来,其最接近的竞争对手,如 Vue 和 Svelte,都引入了使动画更容易实现的 API。而 React 开发者不得不依赖第三方库,例如Motion for React或其他库。

直到现在。

是的,React 正在获得其首个动画 API。天使在歌唱,天堂之门开启,作者塞巴·马克巴格穿破漫漫长夜降临,将 <ViewTransition /> 赐予世人。(顺便说一句,我必须感谢 Seb 回答了我关于这个新 API 的许多问题)

顾名思义,<ViewTransition /> 基于浏览器强大的新视图转换 API功能。

令人兴奋的是,它已在 React 的预发布渠道中提供。因此,在这篇文章中,我们将开始在 React 和 Next.js 中试用 <ViewTransition />,并通过实时的、可复制粘贴的示例探索其功能。

甚至还有一个配套的微型网站您可以在那里尽情浏览这些示例,播放和复制源代码。

但我有点超前了。首先,什么是视图转换?为什么正是 这个 功能促使 React 团队推出了首个动画 API?

视图转换 101

视图转换 API 是一种新的浏览器功能,允许开发者在任意两个视图之间进行动画转换。

它非常强大,允许对以前无法动画化的值进行动画处理,例如在 flex-startflex-end 之间切换 justify-content

注意:接下来的几个示例是用 Motion 的 animateView函数制作的,该函数目前处于 Early Access 阶段。因此,源代码目前仅供Motion+ 会员)

使用)或者在两个完全独立的元素之间进行动画处理,就好像它们是一个元素一样

虽然视图转换在许多方面都很先进,但并非没有缺点

  • 它们是不可中断的(或中断后会出现视觉错误)

  • 伪元素 CSS API 令人不悦

  • 元素动画会改变滚动位置

  • 每个元素都需要一个唯一的 view-transition-name,其管理过程繁琐且容易出错,这使得组合不必要地变得困难(尽管即将推出的 auto 设置将在一定程度上缓解这种情况)。

我们制作了 animateView,开始尽可能多地解决这些痛点。

因此,您可能会认为,太好了,如果您设法为 vanilla JS 用户解决了这些问题,那么下一步应该很容易将 animateView 放入 React 包装器中。

不幸的是,由于视图转换本质上是异步的,因此将 animateView 或浏览器的 document.startViewTransition 与 React 集成存在两个根本问题

  1. 需要在设置状态之前启动视图转换。

  2. 需要将该状态更新包装在 React 的 flushSync

animateView(() => {
  flushSync(() => setState(yourNewState))
})

这是一个性能不佳的组合拳。

您看,视图转换本质上是对包含元素 屏幕截图 的伪元素进行动画处理,而不是对元素本身进行动画处理。这个过程有优点和缺点,这是一个更广泛的讨论,但底线是,这反过来会在视觉上冻结页面的全部或一部分,使其保持静态且不可交互,直到动画 完成之后

因此,启动视图转换的最佳时间是 正好在 您更改 DOM 之前。传统上,这将是 getSnapshotBeforeUpdate 生命周期,但它是同步的,而视图转换是异步的。无法告诉 React 等待视图转换完成页面快照。

所以现在我们被迫在甚至还没有开始状态更新之前启动视图转换,而状态更新将导致渲染,渲染将导致视觉提交。这意味着页面将在视觉上冻结的时间比需要的更长。

更糟糕的是,flushSync 是执行状态更新的最慢方式,因为它会阻塞主线程,直到新状态渲染完成。冻结所有主线程动画和交互,并阻止退出或取消。

这就是 <ViewTransition /> 如此重要的原因。它深入 React 渲染周期的钩子(与 React Hooks 无关)。因此,它可以尽可能晚地触发视图转换,同时在此期间保持页面在视觉上不冻结。

此外,它 适用于异步更新,例如 startTransition<Suspense />,这意味着状态更新可以在动画开始之前被中断或中止。UI 将更具响应性。

哇,听起来很完美,对吧?嗯,除了视图转换 API 本身的内在局限性之外,几乎是完美的。所以现在我们知道了它为什么这么好,让我们深入了解一下。

开始使用

首先:警告!<ViewTransition /> 是一个 实验性 API。它可能(并且很可能会)随时更改,恕不另行通知。这些早期版本的目的是查找 API 中的错误和漏洞。因此,虽然玩起来很有趣,但我今天不会基于此编写生产代码,并且没有立即计划基于此制作 Motion for React API。

话虽如此,最快速的入门方法是fork 这个 CodeSandbox它已经使用 experimental 渠道上的 React 进行了设置。

您也可以像这样在您自己的项目中安装 reactreact-dom

npm install react@experimental react-dom

Next.js 用户必须安装至少 15.2.0-canary.6canary 版本。然后,在您的 next.config 文件中,添加

const nextConfig = {
    experimental: {
        viewTransition: true,
    },
}

最后,作为一个不稳定的 API,ViewTransitionunstable_ViewTransition 的形式导出。因此,您可以像这样导入它

import { unstable_ViewTransition as ViewTransition } from "react"

基本用法

<ViewTransition /> 包装一个组件时,它的第一个 DOM 子元素将被自动分配一个 view-transition-name

例如,此切换开关是通过使用 ViewTransition 包装 .handle 元素来制作的

<button style={{ justifyContent: isOn ? "flex-end" : "flex-start" }}>
  <ViewTransition>
    <div className="handle" />
  </ViewTransition>
</button>

重要的是,isOn 状态更新必须使用 React 的 startTransition 进行包装,否则动画将无法工作。

const toggleOn = startTransition(() => setIsOn(!isOn))

这样做的好处是,这个 view-transition-name 不仅是自动生成的,而且还是 自动应用的

这意味着什么?使用视图转换 API,您不能只是在一个元素上设置 view-transition-name 并忘记它。所有具有 view-transition-name 的元素都将被收集并包含在每个视图转换中。

这意味着,如果采用简单的方法,单击这些开关中的任何一个都会导致六个动画,即使实际上只有一个元素在移动

但是,通过查看检查器,我们可以看到对于这些开关,我们只生成了一个动画。

这是因为 view-transition-name 样式是按需应用和移除的。这对于性能和隔离微交互都非常有用。在 name 生成和应用之间,我们已经解决了视图转换 API 的两个主要痛点。

<ViewTransition /> 在检测视觉变化方面非常强大。在这里,我们只是更改 <img /> 的 URL,并且该组件确保图像正确地从一个淡入淡出到下一个。

切换子元素

view-transition-name 应用的一个强大之处在于,它不仅在元素保持不变时有效。我们可以通过简单地切换出两个完全不同的元素来在它们之间进行交叉淡入淡出。

<ViewTransition>
  {state ? <MenuA /> : <MenuB />}
</ViewTransition>

这甚至适用于 Suspense 组件,因此我们可以从其后备内容动画过渡到其内容(准备就绪时)。

<ViewTransition>
  <Suspense fallback={<Skeleton />}>
    <Content />
  </Suspense>
</ViewTransition>

不幸的是,我无法获得此设置的模拟版本,但如果我成功了,将更新这篇文章。

共享元素动画

在前面的示例中,您是否也注意到了下划线动画?这是通过根据状态在任一按钮中有条件地渲染 <ViewTransition /> 来实现的。我们通过手动提供匹配的 name 属性来链接两者

{isSelected && (
  <ViewTransition name="underline">
    <Underline />
  </ViewTransition>
)}

<ViewTransition /> 在一个位置被移除并在其他位置创建时,这两个元素变为共享。

此功能还具有一个非常巧妙的功能,我绝对会将其复制到 Motion 的 animateView() 函数中。当两个元素像这样链接时,如果其中一个元素位于视口之外,则将使用简单的淡入淡出动画。这样可以防止元素在屏幕上到处飞,而用户却无法从中受益。

为了演示,请尝试按下“Toggle box position”(切换框位置)并注意布局动画,然后按下“Toggle container size”(切换容器大小)以在再次切换框位置之前将底部框移出屏幕

自定义动画

到目前为止,我们已经制作了一堆动画,但我们实际上还没有使用缓动、持续时间或延迟来自定义任何动画。

可以使用 CSS,通过手动设置 name 并使用视图转换 API 的普通繁琐的伪选择器

<>
  <ViewTransition name="photo" />
  <style>{`
    ::view-transition-group(photo),
    ::view-transition-new(photo),
    ::view-transition-old(photo) {
      animation-duration: 1s;
    }
  `}</style>
</>

但也许更有用的是使用该组件方便的事件处理程序的能力。有五个事件处理程序:

  • onEnter/onLeave:此组件正在进入或离开 DOM,并且没有其他组件与其共享 name

  • onLayout:此组件的边界由于外部组件而发生了更改。

  • onUpdate:此组件的内容或边界由于自身或子组件而发生了更改。

  • onShare:此组件正在执行共享元素转换。

每个事件回调都提供一个 ViewTransitionInstance,其中包含对动画中使用的每个伪元素的引用。此引用包含一个预绑定的 Web Animations API 函数,我们可以使用它来创建完全自定义的动画。

因此,为了采用我们的图像交换示例,我们现在可以使用 direction 来动态地将图像动画化到左侧或右侧

 function onUpdate(instance: ViewTransitionInstance) {
      const offset = 100 * direction

      instance.old.animate(
          {
              clipPath: ["none", `translateX(${-offset}px)`],
          },
          { duration: 300, fill: "both", easing: "ease-in" }
      )

      instance.new.animate(
          {
              transform: [`translateX(${offset}px)`, "none"],
          },
          { duration: 400, delay: 200, fill: "both", easing: "ease-out" }
      )
  }

我们也不仅限于典型的 opacity/transform 动画。在这里,我们使用 clipPath 动画来动画化遮罩

这对 Motion for React 意味着什么?

这就是 <ViewTransition /> 的概括。它肯定会为 React 动画解锁许多新功能,并使视图转换 API 比其原始形式更易于使用。

但是,它仅解决了视图转换 API 的 部分 缺点,而视图转换本身并不是解决所有 Web 动画的万能方案。它们“只是”一个很棒的新工具,我们可以将其与我们的其他很棒的工具(如 CSS 过渡、滚动动画等)一起放在我们的工具架上。

然而,Motion for React 确实包含一个 API,该 API 与视图转换在维恩图上非常接近,那就是布局动画.

<motion.div layout />

布局动画执行类似的工作,即对不可能的事物进行动画处理,但使用变换和比例失真校正计算。对于微交互,IMO 它们仍然更受欢迎,部分原因是它们考虑了滚动偏移量的变化,部分原因是它们处理相对/嵌套动画,但主要是因为它们是 可中断的

单击此切换按钮并比较其感觉与本文开头的按钮

明显的缺点是布局动画需要付出约 33kb motion 组件的全部成本。因此,<ViewTransition /> 是一种替代方案,它以无疑更小的捆绑包大小执行许多相同的功能,这是一个好消息。

更让我兴奋的是考虑 Motion 中的 <ViewTransition /> 组件,它可以进一步使所有开发者都能访问视图转换。也许是一个声明式 API,允许使用 JS 缓动函数和弹簧,并包含明智的默认值,类似于 Motion 的其余部分

<AnimateView share={{ type: "spring", bounce: 0.3 }}>

通过再增加几个事件,特别是可以在视图转换开始之前运行的 onRead,我们还可以将一些计划中的 animateView() 增强功能引入 <ViewTransition />,特别是取消滚动位置变化的能力。

在那之前,请告诉我您对 React 的新动画 API 的看法,并告诉我您用它做了什么!

阅读更多

Vue 版 Motion 介绍

Motion 最终登陆 Vue,完整配备了变体、滚动、布局动画以及您喜爱 Framer Motion 的所有其他功能。

Motion 最终登陆 Vue,完整配备了变体、滚动、布局动画以及您喜爱 Framer Motion 的所有其他功能。

如何为您的 Framer 站点添加 cmd-k 搜索快捷方式

默认情况下,Framer Search 组件不支持 cmd-k 键盘快捷方式。以下是如何将其添加到您的 Framer 站点。

默认情况下,Framer Search 组件不支持 cmd-k 键盘快捷方式。以下是如何将其添加到您的 Framer 站点。

Framer Motion 现在是独立的,推出 Motion

Framer Motion 现在是独立的。推出 Motion,一个用于 React 和所有 JavaScript 环境的新动画库。这对您意味着什么。

Framer Motion 现在是独立的。推出 Motion,一个用于 React 和所有 JavaScript 环境的新动画库。这对您意味着什么。

您仍然需要 Framer Motion 吗?

自 Framer Motion 发布以来的五年中,CSS 动画 API 取得了长足的进步。您仍然需要使用 Framer Motion 吗?

自 Framer Motion 发布以来的五年中,CSS 动画 API 取得了长足的进步。您仍然需要使用 Framer Motion 吗?

当浏览器限制 requestAnimationFrame 时

在特定情况下,Safari 和 Firefox 可能会限制 requestAnimationFrame。以下是您的 JavaScript 动画卡顿的原因。

在特定情况下,Safari 和 Firefox 可能会限制 requestAnimationFrame。以下是您的 JavaScript 动画卡顿的原因。

Motion 的实现离不开我们出色的赞助商。

保持关注

订阅以获取最新消息和更新。

保持关注

订阅以获取最新消息和更新。

保持关注

订阅以获取最新消息和更新。