React 中 FLIP 动画你需要了解的一切

Avatar of Kirill Vasiltsov
Kirill Vasiltsov

DigitalOcean 为您的旅程的每个阶段提供云产品。立即开始使用 $200 免费信用额度!

随着最近的 Safari 更新,网页动画 API (WAAPI) 现在在所有现代浏览器(IE 除外)中都无需标志即可支持。 这里有一个 方便的 Pen,您可以在其中检查浏览器支持哪些功能。WAAPI 是进行动画(需要在 JavaScript 中完成)的一种不错的方法,因为它属于原生,这意味着它不需要额外的库即可工作。如果您完全不了解 WAAPI,这里有一个 由 Dan Wilson 编写的非常好的介绍

动画中最有效的方法之一是 FLIP。FLIP 需要一些 JavaScript 代码才能完成其工作。 

让我们看看使用 WAAPI、FLIP 以及将所有这些集成到 React 中的交叉点。但我们先从没有 React 的情况开始,然后再进行到 React。

FLIP 和 WAAPI

FLIP 动画 由 WAAPI 大大简化了!

快速复习一下 FLIP:最大的想法是,您首先将元素放置在您希望它最终到达的位置。接下来,应用转换以将其移动到起始位置。然后取消应用这些转换。 

动画转换非常高效,因此 FLIP 非常高效。在 WAAPI 之前,我们必须直接操作元素的样式以设置转换,并等待下一帧才能将其取消设置/反转

// FLIP Before the WAAPI
el.style.transform = `translateY(200px)`;


requestAnimationFrame(() => {
  el.style.transform = '';
});

许多库都是基于这种方法构建的。 但是,这种方法存在一些问题

  • 一切都感觉像是巨大的 hack。
  • 反转 FLIP 动画极其困难。虽然 CSS 转换在删除类后会“免费”反转,但这在这种情况并非如此。在之前的 FLIP 仍在运行时开始新的 FLIP 会导致故障。反转需要使用getComputedStyles解析转换矩阵并使用它来计算当前尺寸,然后设置新的动画。
  • 高级动画几乎不可能。例如,为了防止缩放父元素的子元素变形,我们需要在每一帧都访问当前缩放值。这只能通过解析转换矩阵来完成。
  • 浏览器有很多陷阱。例如,有时 要在 Firefox 中完美地执行 FLIP 动画需要调用requestAnimationFrame两次
requestAnimationFrame(() => {
  requestAnimationFrame(() => {
    el.style.transform = '';
  });
});

使用 WAAPI 时,我们不会遇到这些问题。反转可以通过 reverse 函数轻松完成。子元素的反向缩放也是可能的。而且,当出现错误时,很容易找出确切的原因,因为我们只使用简单的函数,比如animatereverse,而不是像requestAnimationFrame方法那样逐一梳理。

以下是 WAAPI 版本的概述

el.classList.toggle('someclass');
const keyframes = /* Calculate the size/position diff */;
el.animate(keyframes, 2000);

FLIP 和 React

要了解 FLIP 动画在 React 中的工作原理,必须了解它们如何,最重要的是为什么它们在纯 JavaScript 中有效。回忆一下 FLIP 动画的结构

Diagram. Cache current site and position, make a style change, get new size and position, calculate the difference, set transforms, and cancel transforms. Each item has a purple background, except the last one, indicating they happen before paint.

所有带有紫色背景的内容都必须在渲染的“绘制”步骤之前完成。否则,我们会看到新的样式短暂闪烁,这不好。在 React 中情况会变得更加复杂,因为所有 DOM 更新都由我们完成。

FLIP 动画的神奇之处在于,元素在浏览器有机会绘制之前就进行转换。那么如何在 React 中确定“绘制之前”的时刻呢?

认识一下 useLayoutEffect hook。如果您想知道它有什么用,这就是它!我们在回调中传递的任何内容都将在 DOM 更新之后绘制之前同步执行。换句话说,这是设置 FLIP 的绝佳位置!

让我们做一些 FLIP 技术非常擅长的:动画化 DOM 位置。如果我们要动画化元素如何从一个 DOM 位置移动到另一个 DOM 位置,CSS 无能为力。(想象一下,在待办事项列表中完成一项任务并将其移至“已完成”任务列表,就像在下面的 Pen 中单击项目时一样。)

让我们看一下最简单的示例。单击下面 Pen 中的两个正方形中的任何一个,它们都会交换位置。如果没有 FLIP,它会立即发生。

那里发生了很多事情。注意所有工作是如何在生命周期挂钩回调中完成的:useEffectuseLayoutEffect。让它有点令人困惑的是,FLIP 动画的时间轴不是从代码本身就能看出来的,因为它跨越了两次 React 渲染。以下是 React FLIP 动画的结构,以显示操作的不同顺序

Diagram. Cache the size and position, retrieve previous size and position from cache, get new size and position, calculate the difference, and play the animation.

虽然useEffect总是运行在useLayoutEffect和浏览器绘制之后,但重要的是我们必须在第一次渲染后缓存元素的位置和大小。我们没有机会在第二次渲染时这样做,因为useLayoutEffect运行在所有 DOM 更新之后。但该过程本质上与 vanilla FLIP 动画相同。

注意事项

像大多数事情一样,在 React 中使用 FLIP 时,有一些注意事项需要考虑。

保持在 100 毫秒以内

FLIP 动画是计算。计算需要时间,并且在您能够显示流畅的 60fps 转换之前,您需要做相当多的工作。如果延迟低于 100 毫秒,人们不会注意到,所以确保一切都低于这个值。DevTools 中的“性能”选项卡是检查此问题的理想位置。

不必要的渲染

我们不能使用 useState 来缓存大小、位置和动画对象,因为每次setState都会导致不必要的渲染,从而减慢应用程序的速度。在最坏的情况下,甚至会导致错误。尝试改为使用useRef,将其视为可以被修改而不渲染任何内容的对象。

布局抖动

避免反复触发浏览器布局。在 FLIP 动画的上下文中,这意味着避免循环遍历元素并使用getBoundingClientRect读取它们的位置,然后立即使用 animate 对它们进行动画化。尽可能地批处理“读取”和“写入”。这将使动画极其流畅。

动画取消

尝试在 之前的演示 中随机单击正方形,当它们移动时,然后再单击一次,当它们停止时。您会看到故障。在现实生活中,用户会在元素移动时与它们进行交互,因此值得确保它们被取消、暂停并流畅地更新。 

但是,并非所有动画都可以使用reverse反转。有时,我们希望它们停止,然后移动到新的位置(就像随机打乱元素列表时一样)。在这种情况下,我们需要

  • 获取正在移动元素的大小/位置
  • 完成当前动画
  • 计算新的尺寸和位置差异
  • 启动新的动画

在 React 中,这可能比看起来要难。我浪费了很多时间与之作斗争。必须缓存当前动画对象。一个好方法是创建一个Map,以便通过 ID 获取动画。然后,我们需要获取正在移动元素的大小和位置。有两种方法可以做到

  1. 使用函数组件:只需在函数主体中循环遍历所有动画元素,并缓存当前位置。
  2. 使用类组件:使用 getSnapshotBeforeUpdate 生命周期方法。

事实上,React 官方文档建议使用 getSnapshotBeforeUpdate,“因为在“渲染”阶段的生命周期(如 render)和“提交”阶段的生命周期(如 getSnapshotBeforeUpdatecomponentDidUpdate)之间可能存在延迟。”但是,目前还没有这个方法的 Hook 等价物。我发现使用函数组件的主体已经足够了。

不要与浏览器对抗

我之前说过,避免与浏览器对抗,并尝试按照浏览器的方式做事。如果我们需要为简单的尺寸变化添加动画,那么请考虑 CSS 是否足够(例如  transform: scale())。我发现 FLIP 动画最适合浏览器无法真正帮助的情况

  • 为 DOM 位置变化添加动画(如我们上面所做的那样)
  • 共享布局动画

第二个是第一个的更复杂版本。有两个 DOM 元素表现和看起来像一个元素,并改变其位置(而另一个元素被卸载/隐藏)。这种技巧可以实现一些很酷的动画。例如,这个动画是由我构建的一个名为 react-easy-flip 的库使用这种方法实现的

有相当多的库使 React 中的 FLIP 动画更容易,并抽象了样板代码。当前积极维护的库包括:react-flip-toolkit 和我的库,react-easy-flip

如果你不介意使用更重量级的库,但它可以实现更通用的动画,查看 framer-motion。它还可以实现很酷的共享布局动画! 这里有一段视频深入探讨了该库。


资源和参考文献