前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实战
前言
在 Vue 的官网中的过渡动画章节中,可以看到一个很酷炫的动画效果

乍一看,让我们手写出这个逻辑应该是非常复杂的,先看看本文最后要实现的效果吧,和这个案例是非常类似的。
预览

分析需求
拿到了这个需求,第一直觉是怎么做?假设第一行第一个图片移动到了第二行第三列,是不是要计算出第一行的高度,再计算出第二行前两个元素的宽度,然后从初始的坐标点通过 CSS 或者一些动画 API 移动过去?这样做是可以,但是在图片不定高不定宽,并且一次要移动很多图片情况下,这个计算方法就非常复杂了。并且这种情况下,图片的坐标都需要我们手动管理,非常不利于维护和扩展。
换种思路,能不能直接很自然的把 DOM 元素通过原生 API 添加到 DOM 树中,然后让浏览器帮我们好这个终点值,最后我们再动画位移过去?
在文档里我们发现一个名词:FLIP
,这给了我们一个线索,是不是用这个玩意就可以写出这个动画呢?
答案是肯定的
FLIP
FLIP
究竟是什么东西呢?先看下它的定义:
First
即将做动画的元素的初始状态(比如位置、透明度等等)。
Last
即将做动画的元素的最终状态。
Invert
这一步比较关键,假设我们图片的初始位置是 左: 0, 上:0
,元素动画后的最终位置是 左:100, 上100
,那么很明显这个元素是向右下角运动了 100px
。
但是,此时我们不按照常规思维去先计算它的最终位置,然后再命令元素从 0, 0
运动到 100, 100
,而是先让元素自己移动过去(比如在 Vue 中用数据来驱动,在数组前面追加几个图片,之前的图片就自己移动到下面去了)。
这里有一个关键的知识点要注意了。
DOM 元素属性的改变(比如 left
、right
、 transform
等等),会被集中起来延迟到浏览器的下一帧统一渲染,所以我们可以得到一个这样的中间时间点:DOM 状态(位置信息)改变了,而浏览器还没渲染。
有了这个前置条件,我们就可以保证先让 Vue 去操作 DOM 变更,此时浏览器还未渲染,我们已经能得到 DOM 状态变更后的位置了。
说的具体点,假设我们的图片是一行两个排列,图片数组初始化的状态是 [img1, img2
,此时我们往数组头部追加两个元素 [img3, img4, img1, img2]
,那么 img1
和 img2
就自然而然的被挤到下一行去了。
假设 img1
的初始位置是 0, 0
,被数据驱动导致的 DOM 改变挤下去后的位置是 100, 100
,那么此时浏览器还没有渲染,我们可以在这个时间点把 img1.style.transform = translate(-100px, -100px)
,让它 先 Invert 倒置回位移前的位置。
Play
倒置了以后,想要让它做动画就很简单了,再让它回到 0, 0
的位置即可,本文会采用最新的 Web Animation API
来实现最后的 Play
。
实现
首先图片渲染很简单,就让图片通过简单的排成 4 列即可:
- .wrap {
- display: flex;
- flex-wrap: wrap;
- }
- .img {
- width: %;
- }
- <div v-else class="wrap">
- <div class="img-wrap" v-for="src in imgs" :key="src">
- <img ref="imgs" class="img" :src="src" />
- </div>
- </div>
那么关键点就在于怎么往这个 imgs
数组里追加元素后,做一个流畅的路径动画。
我们来实现追加图片的方法 add
:
- async add() {
- const newData = this.getSister()
- await preload(newData)
- }
首先随机的取出几张图片作为待放入数组的元素,利用 new Image
预加载这些图片,防止渲染一堆空白图片到屏幕上。
然后定义一个计算一组 DOM 元素位置的函数 getRects
,利用 getBoundingClientRect
可以获得最新的位置信息,这个方法在接下来获取图片元素旧位置和新位置时都要使用。
- function getRects(doms) {
- return doms.map((dom) => {
- const rect = dom.getBoundingClientRect()
- const { left, top } = rect
- return { left, top }
- })
- }
- // 当前已有的图片
- const prevImgs = this.$refs.imgs.slice()
- const prevPositions = getRects(prevImgs)
记录完图片的旧位置后,就可以向数组里追加新的图片了:
- this.imgs = newData.concat(this.imgs)
随后就是比较关键的点了,我们知道 Vue 是异步渲染的,也就是改变了这个 imgs
数组后不会立刻发生 DOM 的变动,此时我们要用到 nextTick
这个 API,这个 API 把你传入的回调函数放进了 microTask
队列,正如上文提到的事件循环的文章里所说,microTask
队列的执行一定发生在浏览器重新渲染前。
由于先调用了 this.imgs = newData.concat(this.imgs)
这段代码,触发了 Vue 的响应式依赖更新,此时 Vue 内部会把本次 DOM 更新的渲染函数先放到 microTask
队列中,此时的队列是[changeDOM]
。
调用了 nextTick(callback)
后,这个callback
函数也会被追加到队列中,此时的队列是 [changeDOM, callback]
。
这下聪明的你肯定就明白了,为什么 nextTick
的回调函数里一定能获取到最新的 DOM 元素,此时新加入图片的 DOM 已经在 DOM 树中,但是屏幕还没有发生绘制,调用 getBoundingClientRect
可以触发「强制同步布局」,此后的代码里就可以获取到 DOM 在屏幕预计出现的准确位置。(注意,位置信息是最新的,但是屏幕上并没有发生绘制)
由于我们之前保存了图片元素节点的数组 prevImgs
,所以在 nextTick
里对这些旧图片再调用一次 getRect
方法,获取到的就是旧图片的最新位置了。
- async add() {
- // 最新 DOM 状态
- this.$nextTick(() => {
- // 再调用同样的方法获取最新的元素位置
- const currentPositions = getRects(prevImgs)
- })
- },
此时我们已经拥有了
Invert
步骤的关键信息,新位置和旧位置,那么接下来就很简单了,把图片数组循环做一个倒置后Play
的动画即可。
- prevImgs.forEach((imgRef, imgIndex) => {
- const currentPosition = currentPositions[imgIndex]
- const prevPosition = prevPositions[imgIndex]
- // 倒置后的位置,虽然图片移动到最新位置了,但你先给我回去,等着我来让你做动画。
- const invert = {
- left: prevPosition.left - currentPosition.left,
- top: prevPosition.top - currentPosition.top,
- }
- const keyframes = [
- // 初始位置是倒置后的位置
- {
- transform: `translate(${invert.left}px, ${invert.top}px)`,
- },
- // 图片更新后本来应该在的位置
- { transform: "translate(0)" },
- ]
- const options = {
- duration: ,
- easing: "cubic-bezier(0,0,0.32,1)",
- }
- // 开始运动!
- const animation = imgRef.animate(keyframes, options)
- })
此时一个非常流畅的路径动画效果就完成了。
- 目前,我也组建了一个自学群,可以一起讨论研究前端的各个事宜,以及提高能力的方法,只要你想了解前端,精通前端,都欢迎你们加入我们的前端自学。
- 你可以找到志同道合的朋友,相互激励的学习伙伴,还能得到大神的经验分享,和加入项目实战的机会。这是我的WEB前端q裙。---
- 整理了-套最新的前端基础教程,学习前端的这个过程当中我也收集了很多前端学习手册,面试题,开发工具,PDF文档书籍教程,可以直接分享给你们。
完整实现如下:
- async add() {
- const newData = this.getSister()
- await preload(newData)
- const prevImgs = this.$refs.imgs.slice()
- const prevPositions = getRects(prevImgs)
- this.imgs = newData.concat(this.imgs)
- this.$nextTick(() => {
- const currentPositions = getRects(prevImgs)
- prevImgs.forEach((imgRef, imgIndex) => {
- const currentPosition = currentPositions[imgIndex]
- const prevPosition = prevPositions[imgIndex]
- const invert = {
- left: prevPosition.left - currentPosition.left,
- top: prevPosition.top - currentPosition.top,
- }
- const keyframes = [
- {
- transform: `translate(${invert.left}px, ${invert.top}px)`,
- },
- { transform: "translate(0)" },
- ]
- const options = {
- duration: ,
- easing: "cubic-bezier(0,0,0.32,1)",
- }
- const animation = imgRef.animate(keyframes, options)
- })
- })
- },
乱序
现在我们想要实现官网 demo 中的 shuffle
效果,有了追加图片逻辑的铺垫,是不是已经觉得思路如泉涌了?没错,即使图片被打乱的再厉害,只要我们有「图片开始时的位置」和「图片结束时的位置」,那就可以轻松做到路径动画。
现在我们需要做的是把动画的逻辑抽离出来,我们分析一下整条链路:
保存旧位置 -> 改变数据驱动视图更新 -> 获得新位置 -> 利用 FLIP 做动画
其实外部只需要传入一个 update
方法告诉我们如何去更新图片数组,就可以把这个逻辑完全抽象到一个函数里去。
- scheduleAnimation(update) {
- // 获取旧图片的位置
- const prevImgs = this.$refs.imgs.slice()
- const prevSrcRectMap = createSrcRectMap(prevImgs)
- // 更新数据
- update()
- // DOM更新后
- this.$nextTick(() => {
- const currentSrcRectMap = createSrcRectMap(prevImgs)
- Object.keys(prevSrcRectMap).forEach((src) => {
- const currentRect = currentSrcRectMap[src]
- const prevRect = prevSrcRectMap[src]
- const invert = {
- left: prevRect.left - currentRect.left,
- top: prevRect.top - currentRect.top,
- }
- const keyframes = [
- {
- transform: `translate(${invert.left}px, ${invert.top}px)`,
- },
- { transform: "" },
- ]
- const options = {
- duration: ,
- easing: "cubic-bezier(0,0,0.32,1)",
- }
- const animation = currentRect.img.animate(keyframes, options)
- })
- })
- }
那么追加图片和乱序的函数就变得非常简单了:
- // 追加图片
- async add() {
- const newData = this.getSister()
- await preload(newData)
- this.scheduleAnimation(() => {
- this.imgs = newData.concat(this.imgs)
- })
- },
- // 乱序图片
- shuffle() {
- this.scheduleAnimation(() => {
- this.imgs = shuffle(this.imgs)
- })
- }
总结
FLIP
FLIP 不光可以做位置变化的动画,对于透明度、宽高等等也一样可以很轻松的实现。
比如电商平台中经常会出现一个动画,点击一张商品图片后,商品从它本来的位置慢慢的放大成了一张完整的页面。
FLIP
的思路掌握后,只要你知道元素动画前的状态和元素动画后的状态,你都可以轻松的通过「倒置状态」后,让它们做一个流畅的动画后到达目的地,并且此时的 DOM 状态是很干净的,而不是通过大量计算的方式强迫它从 0, 0
位移到 100, 100
,并且让 DOM 样式上留下 transform: translate(100px, 100px)
类似的字样。
Web Animation
利用 Web Animation API
可以让我们用 JavaScript 更加直观的描述我们需要元素去做的动画,想象一下这个需求如果用 CSS 来做,我们大概会这样去完成这个需求:
- const currentImgStyle = currentRect.img.style
- currentImgStyle.transform = `translate(${invert.left}px, ${invert.top}px)`
- currentImgStyle.transitionDuration = "0s"
- this._reflow = document.body.offsetHeight
- currentRect.img.classList.add("move")
- currentImgStyle.transform = currentRect.img.style.transitionDuration = ""
- currentRect.img.addEventListener("transitionend", () => {
- currentRect.img.classList.remove("move")
- })
这是选择用比较原生的方式去控制 CSS 样式实现的 FLIP 动画,这段代码让我觉得不舒服的点在于:
- 需要通过
class
的增加和删除来和 CSS 来进行交互,整体流程不太符合直觉。 - 需要监听动画完成事件,并且做一些清理操作,容易遗漏。
- 需要利用
document.body.offsetHeight
这样的方式触发强制同步布局
,比较 hack 的知识点。 - 需要利用
this._reflow = document.body.offsetHeight
这样的方式向元素实例上增加一个没有意义的属性,防止被 Rollup 等打包工具tree-shaking
误删。 比较 hack 的知识点 +1。
而利用 Web Animation API
的代码则变得非常符合直觉和易于维护:
- const keyframes = [
- {
- transform: `translate(${invert.left}px, ${invert.top}px)`,
- },
- { transform: "" },
- ]
- const options = {
- duration: ,
- easing: "cubic-bezier(0,0,0.32,1)",
- }
- const animation = currentRect.img.animate(keyframes, options)
关于兼容性问题,W3C 已经提供了 Web Animation API Polyfill
,可以放心大胆的使用。
期待在不久的未来,我们可以抛弃旧的动画模式,迎接这种更新更好的 API。
希望这篇文章能让对动画发愁的你有一些收获,谢谢!
前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实战的更多相关文章
- 2015 前端[JS]工程师必知必会
2015 前端[JS]工程师必知必会 本文摘自:http://zhuanlan.zhihu.com/FrontendMagazine/20002850 ,因为好东东西暂时没看懂,所以暂时保留下来,供以 ...
- [ 学习路线 ] 2015 前端(JS)工程师必知必会 (2)
http://segmentfault.com/a/1190000002678515?utm_source=Weibo&utm_medium=shareLink&utm_campaig ...
- python网络爬虫,知识储备,简单爬虫的必知必会,【核心】
知识储备,简单爬虫的必知必会,[核心] 一.实验说明 1. 环境登录 无需密码自动登录,系统用户名shiyanlou 2. 环境介绍 本实验环境采用带桌面的Ubuntu Linux环境,实验中会用到桌 ...
- Android程序员必知必会的网络通信传输层协议——UDP和TCP
1.点评 互联网发展至今已经高度发达,而对于互联网应用(尤其即时通讯技术这一块)的开发者来说,网络编程是基础中的基础,只有更好地理解相关基础知识,对于应用层的开发才能做到游刃有余. 对于Android ...
- 迈向高阶:优秀Android程序员必知必会的网络基础
1.前言 网络通信一直是Android项目里比较重要的一个模块,Android开源项目上出现过很多优秀的网络框架,从一开始只是一些对HttpClient和HttpUrlConnection简易封装使用 ...
- 脑残式网络编程入门(三):HTTP协议必知必会的一些知识
本文原作者:“竹千代”,原文由“玉刚说”写作平台提供写作赞助,原文版权归“玉刚说”微信公众号所有,即时通讯网收录时有改动. 1.前言 无论是即时通讯应用还是传统的信息系统,Http协议都是我们最常打交 ...
- RecyclerView 必知必会(转)
[腾讯Bugly干货分享]RecyclerView 必知必会 本文来自于腾讯Bugly公众号(weixinBugly),未经作者同意,请勿转载,原文地址:http://mp.weixin.qq.com ...
- MySql必知必会内容导图
<MySQL必知必会>从介绍简单的数据检索开始,逐步深入一些复杂的内容,包括联结的使用.子查询.正则表达式和基于全文本的搜索.存储过程.游标.触发器.表约束,等等.通过重点突出的章节,条理 ...
- 读书笔记汇总 - SQL必知必会(第4版)
本系列记录并分享学习SQL的过程,主要内容为SQL的基础概念及练习过程. 书目信息 中文名:<SQL必知必会(第4版)> 英文名:<Sams Teach Yourself SQL i ...
随机推荐
- Java实现 蓝桥杯VIP 算法提高 勾股数
算法提高 勾股数 时间限制:1.0s 内存限制:256.0MB 问题描述 勾股数是一组三个自然数,a < b < c,以这三个数为三角形的三条边能够形成一个直角三角形 输出所有a + b ...
- PAT 数字黑洞
给定任一个各位数字不完全相同的 4 位正整数,如果我们先把 4 个数字按非递增排序,再按非递减排序,然后用第 1 个数字减第 2 个数字,将得到一个新的数字.一直重复这样做,我们很快会停在有“数字黑洞 ...
- 分享一个新出炉的JVM里不痛不痒的BUG(Attach机制相关)
本文来自: PerfMa技术社区 PerfMa(笨马网络)官网 概述 老早之前写过一篇文章,关于attach机制的,可以看下这篇老文章了解一下JVM源码分析之Attach机制实现完全解读,比如大家常用 ...
- (十)HTTP.sys远程代码执行
01 漏洞描述 上篇文章介绍了Host头攻击,今天我们讲一讲HTTP.sys远程代码执行漏洞. HTTP.sys是Microsoft Windows处理HTTP请求的内核驱动程序,为了优化IIS服务器 ...
- KVM虚拟机使用NAT+iptables做端口映射
环境介绍 有一个KVM宿主机,一个外网IP绑定在了宿主服务器上,但是希望直接用ssh访问上面的所有虚拟机,还想虚拟机提供外网服务, 解决方法如下: 环境为RHEL6.3,外网IP为 61.155.xx ...
- ElasticSearch系列(二):ElasticSearch Head、Kibana、Elasticsearch-Analysis-Ik安装、使用
1.ElasticSearch Head使用 扩展程序安装插件:ElasticSearch github地址:https://github.com/mobz/elasticsearch-head/ 运 ...
- java第三阶段作业总结
Java第三阶段总结 前言 到这里,Java课程学习进入了尾声,在这学习过程中,我学习到很多,也发现了自己的很多不足,这篇博客主要针对的是Java整门课程学习的总结. 课程收获 对整门课程的学习,我有 ...
- 3、react-props/state
1.react中属性props和状态state 属性--静态得,所以在初始化得时候使用得是static进行初始化得,正常情况下属性不改 状态--动态得,它得值是可以发生改变得,react中的组件更新( ...
- error: RPC failed; curl 18 transfer closed with outstanding read data remaining的解决
解决方案也是网上搜的,总结一下 一,加大缓存区git config --global http.postBuffer 524288000这个大约是500M二.少clone一些,–depth 1git ...
- vs2010静态编译qt5.1.0
本博文参考 http://blog.chinaunix.net/uid-20690340-id-3802197.html 静态库在链接的时候直接写入二进制文件里,这样的好处在于发布的时候无需附带dll ...