介绍
FLIP(First last Invert Play) 是一种实现动画
的思路。只利用transform
或 opacity
模拟布局变动的技巧,不触发Layout
,相比使用top
、left
来做动画有较好的性能,当有多个元素触发动画时不会卡顿。
![](https://file.jishuzhan.net/article/1772864566066155521/429d42876944501b05f5d34b4752e3fb.webp)
html 代码结构
html
<div class="test">test</div>
<div class="list">
<div class="item">1</div>
<div class="item">2</div>
<div class="item">3</div>
<div class="item">4</div>
</div>
First 记录要监控的元素位置
这里要改变 item2
的位置,所以先记录下 item2
的位置信息,这里记录 item2
距离视口的位置:x
, y
。
js
const list = document.querySelector(".list");
const item2 = list.children[1];
function FLIP() {
const { x: startX, y: startY } = item2.getBoundingClientRect();
...
}
Last 记录监控的元素变化后位置
如果此时在第二行打断点,可以发现 item2
还没有被插入到末尾,但是可以获取到其在末尾时的位置信息。这是因为 getBoundingClientRect
函数触发了浏览器的回流,使浏览器重新计算了元素位置,而此时主线程
还被js代码占用,所以处于渲染线程
的重绘
还没开始,就导致上述所说情况。
js
list.insertBefore(item2, null); // 第二个参数为 null 时,元素会被插入到末尾
const { x: endX, y: endY } = item2.getBoundingClientRect();
Invert 移动元素到 First 的位置
js
item2.style.transform = `translate(${startX - endX}px, ${startY - endY}px)`;
Play 使用动画还原元素到 Last 的位置
当 transform
属性被删除时,item2
会回到 Last
的位置
js
requestAnimationFrame(() => {
item2.style.transition = "transform 1s";
item2.style.removeProperty("transform");
})
动画效果
![](https://file.jishuzhan.net/article/1772864566066155521/31a1a457a3cee7fb75216b6ed433f35b.webp)
js
const list = document.querySelector(".list");
const item2 = list.children[1];
function FLIP() {
const { x: startX, y: startY } = item2.getBoundingClientRect();
list.insertBefore(item2, null);
const { x: endX, y: endY } = item2.getBoundingClientRect();
item2.style.transform = `translate(${startX - endX}px, ${startY - endY}px)`;
requestAnimationFrame(() => {
item2.style.transition = "transform 1s";
item2.style.removeProperty("transform");
});
}
const test = document.querySelector(".test");
test.addEventListener("click", FLIP);
可以看到只有 item2
元素应用了动画效果,其他元素也想应用动画的话,就得一个个设置,所以得改造下 FLIP
函数。
完整代码
ts
class FLIP {
constructor(list, duration = 0.5) {
this.duration = duration;
this.list = [...list];
this.positionMap = new WeakMap();
this.list.forEach((item) => {
const { x, y } = item.getBoundingClientRect();
this.positionMap.set(item, [x, y]);
});
}
play() {
this.list.forEach((item, index) => {
item.style.removeProperty("transition");
const position = this.positionMap.get(item);
if (!position) return;
const [startX = 0, startY = 0] = position;
const { x: endX, y: endY } = item.getBoundingClientRect();
item.style.transform = `translate(${startX - endX}px, ${startY - endY}px)`;
this.positionMap.set(item, [endX, endY]);
this.raf(() => {
item.style.transition = `transform ${this.duration}s ease-out`;
item.style.removeProperty("transform");
});
});
}
raf(fn) {
requestAnimationFrame(() => {
requestAnimationFrame(fn.bind(this));
});
}
}
const list = document.querySelector(".list");
const flip = new FLIP(list.children);
const test = document.querySelector(".test");
test.addEventListener("click", move);
function move() {
const item2 = list.children[1];
list.insertBefore(item2, null);
flip.play();
}
改造完后只需要在 new FLIP
时传入要监控的元素列表即可,在元素位置发生变化后调用 flip.play()
,即可对所有元素应用动画。
![](https://file.jishuzhan.net/article/1772864566066155521/7ea9917d4b41f1b04a8f452d84792cb9.webp)