callback回调使用 vue_前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实战
前言
在 Vue 的官網(wǎng)中的過渡動(dòng)畫章節(jié)中,可以看到一個(gè)很酷炫的動(dòng)畫效果
乍一看,讓我們手寫出這個(gè)邏輯應(yīng)該是非常復(fù)雜的,先看看本文最后要實(shí)現(xiàn)的效果吧,和這個(gè)案例是非常類似的。
預(yù)覽
也可以直接進(jìn)預(yù)覽網(wǎng)址里看:
http://sl1673495.gitee.io/flip-animation
圖片素材依然引用自知乎問題《有個(gè)漂亮女朋友是種怎樣的體驗(yàn)?》,侵刪。
分析需求
拿到了這個(gè)需求,第一直覺是怎么做?假設(shè)第一行第一個(gè)圖片移動(dòng)到了第二行第三列,是不是要計(jì)算出第一行的高度,再計(jì)算出第二行前兩個(gè)元素的寬度,然后從初始的坐標(biāo)點(diǎn)通過 CSS 或者一些動(dòng)畫 API 移動(dòng)過去?這樣做是可以,但是在圖片不定高不定寬,并且一次要移動(dòng)很多圖片情況下,這個(gè)計(jì)算方法就非常復(fù)雜了。并且這種情況下,圖片的坐標(biāo)都需要我們手動(dòng)管理,非常不利于維護(hù)和擴(kuò)展。
換種思路,能不能直接很自然的把 DOM 元素通過原生 API 添加到 DOM 樹中,然后讓瀏覽器幫我們好這個(gè)終點(diǎn)值,最后我們?cè)賱?dòng)畫位移過去?
在文檔里我們發(fā)現(xiàn)一個(gè)名詞:FLIP,這給了我們一個(gè)線索,是不是用這個(gè)玩意就可以寫出這個(gè)動(dòng)畫呢?
答案是肯定的,順著這個(gè)線索找到 Aerotwist 社區(qū)里的一篇文章:flip-your-animations,以這篇文章為切入點(diǎn),一步步來(lái)實(shí)現(xiàn)一個(gè)類似的效果。
FLIP
FLIP 究竟是什么東西呢?先看下它的定義:
First
即將做動(dòng)畫的元素的初始狀態(tài)(比如位置、透明度等等)。
Last
即將做動(dòng)畫的元素的最終狀態(tài)。
Invert
這一步比較關(guān)鍵,假設(shè)我們圖片的初始位置是 左: 0, 上:0,元素動(dòng)畫后的最終位置是 左:100, 上100,那么很明顯這個(gè)元素是向右下角運(yùn)動(dòng)了 100px。
但是,此時(shí)我們不按照常規(guī)思維去先計(jì)算它的最終位置,然后再命令元素從 0, 0 運(yùn)動(dòng)到 100, 100,而是先讓元素自己移動(dòng)過去(比如在 Vue 中用數(shù)據(jù)來(lái)驅(qū)動(dòng),在數(shù)組前面追加幾個(gè)圖片,之前的圖片就自己移動(dòng)到下面去了)。
這里有一個(gè)關(guān)鍵的知識(shí)點(diǎn)要注意了,也是我在之前的文章《深入解析你不知道的 EventLoop 和瀏覽器渲染、幀動(dòng)畫、空閑回調(diào)》中提到過的:
DOM 元素屬性的改變(比如 left、right、 transform 等等),會(huì)被集中起來(lái)延遲到瀏覽器的下一幀統(tǒng)一渲染,所以我們可以得到一個(gè)這樣的中間時(shí)間點(diǎn):DOM 狀態(tài)(位置信息)改變了,而瀏覽器還沒渲染。
有了這個(gè)前置條件,我們就可以保證先讓 Vue 去操作 DOM 變更,此時(shí)瀏覽器還未渲染,我們已經(jīng)能得到 DOM 狀態(tài)變更后的位置了。
說的具體點(diǎn),假設(shè)我們的圖片是一行兩個(gè)排列,圖片數(shù)組初始化的狀態(tài)是 [img1, img2,此時(shí)我們往數(shù)組頭部追加兩個(gè)元素 [img3, img4, img1, img2],那么 img1 和 img2 就自然而然的被擠到下一行去了。
假設(shè) img1 的初始位置是 0, 0,被數(shù)據(jù)驅(qū)動(dòng)導(dǎo)致的 DOM 改變擠下去后的位置是 100, 100,那么此時(shí)瀏覽器還沒有渲染,我們可以在這個(gè)時(shí)間點(diǎn)把 img1.style.transform = translate(-100px, -100px),讓它 先 Invert 倒置回位移前的位置。
Play
倒置了以后,想要讓它做動(dòng)畫就很簡(jiǎn)單了,再讓它回到 0, 0 的位置即可,本文會(huì)采用最新的 Web Animation API 來(lái)實(shí)現(xiàn)最后的 Play。
MDN 文檔:Web Animation
實(shí)現(xiàn)
首先圖片渲染很簡(jiǎn)單,就讓圖片通過簡(jiǎn)單的排成 4 列即可:
.wrap {display: flex;flex-wrap: wrap; }.img {width: 25%; }<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>那么關(guān)鍵點(diǎn)就在于怎么往這個(gè) imgs 數(shù)組里追加元素后,做一個(gè)流暢的路徑動(dòng)畫。
我們來(lái)實(shí)現(xiàn)追加圖片的方法 add:
async add() {const newData = this.getSister()await preload(newData) }首先隨機(jī)的取出幾張圖片作為待放入數(shù)組的元素,利用 new Image 預(yù)加載這些圖片,防止渲染一堆空白圖片到屏幕上。
然后定義一個(gè)計(jì)算一組 DOM 元素位置的函數(shù) getRects,利用 getBoundingClientRect 可以獲得最新的位置信息,這個(gè)方法在接下來(lái)獲取圖片元素舊位置和新位置時(shí)都要使用。
function getRects(doms) {return doms.map((dom) => {const rect = dom.getBoundingClientRect()const { left, top } = rectreturn { left, top }}) }// 當(dāng)前已有的圖片 const prevImgs = this.$refs.imgs.slice() const prevPositions = getRects(prevImgs)記錄完圖片的舊位置后,就可以向數(shù)組里追加新的圖片了:
this.imgs = newData.concat(this.imgs)隨后就是比較關(guān)鍵的點(diǎn)了,我們知道 Vue 是異步渲染的,也就是改變了這個(gè) imgs 數(shù)組后不會(huì)立刻發(fā)生 DOM 的變動(dòng),此時(shí)我們要用到 nextTick 這個(gè) API,這個(gè) API 把你傳入的回調(diào)函數(shù)放進(jìn)了 microTask 隊(duì)列,正如上文提到的事件循環(huán)的文章里所說,microTask隊(duì)列的執(zhí)行一定發(fā)生在瀏覽器重新渲染前。
由于先調(diào)用了 this.imgs = newData.concat(this.imgs) 這段代碼,觸發(fā)了 Vue 的響應(yīng)式依賴更新,此時(shí) Vue 內(nèi)部會(huì)把本次 DOM 更新的渲染函數(shù)先放到 microTask隊(duì)列中,此時(shí)的隊(duì)列是[changeDOM]。
調(diào)用了 nextTick(callback) 后,這個(gè)callback函數(shù)也會(huì)被追加到隊(duì)列中,此時(shí)的隊(duì)列是 [changeDOM, callback]。
這下聰明的你肯定就明白了,為什么 nextTick的回調(diào)函數(shù)里一定能獲取到最新的 DOM 元素,此時(shí)新加入圖片的 DOM 已經(jīng)在 DOM 樹中,但是屏幕還沒有發(fā)生繪制,調(diào)用 getBoundingClientRect 可以觸發(fā)「強(qiáng)制同步布局」,此后的代碼里就可以獲取到 DOM 在屏幕預(yù)計(jì)出現(xiàn)的準(zhǔn)確位置。(注意,位置信息是最新的,但是屏幕上并沒有發(fā)生繪制)
由于我們之前保存了圖片元素節(jié)點(diǎn)的數(shù)組 prevImgs,所以在 nextTick 里對(duì)這些舊圖片再調(diào)用一次 getRect 方法,獲取到的就是舊圖片的最新位置了。
async add() {// 最新 DOM 狀態(tài)this.$nextTick(() => {// 再調(diào)用同樣的方法獲取最新的元素位置const currentPositions = getRects(prevImgs)}) },此時(shí)我們已經(jīng)擁有了 Invert 步驟的關(guān)鍵信息,新位置和舊位置,那么接下來(lái)就很簡(jiǎn)單了,把圖片數(shù)組循環(huán)做一個(gè)倒置后 Play的動(dòng)畫即可。
prevImgs.forEach((imgRef, imgIndex) => {const currentPosition = currentPositions[imgIndex]const prevPosition = prevPositions[imgIndex]// 倒置后的位置,雖然圖片移動(dòng)到最新位置了,但你先給我回去,等著我來(lái)讓你做動(dòng)畫。const invert = {left: prevPosition.left - currentPosition.left,top: prevPosition.top - currentPosition.top,}const keyframes = [// 初始位置是倒置后的位置{transform: `translate(${invert.left}px, ${invert.top}px)`,},// 圖片更新后本來(lái)應(yīng)該在的位置{ transform: "translate(0)" },]const options = {duration: 300,easing: "cubic-bezier(0,0,0.32,1)",}// 開始運(yùn)動(dòng)!const animation = imgRef.animate(keyframes, options) })此時(shí)一個(gè)非常流暢的路徑動(dòng)畫效果就完成了。
完整實(shí)現(xiàn)如下:
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: 300,easing: "cubic-bezier(0,0,0.32,1)",}const animation = imgRef.animate(keyframes, options)})}) },亂序
現(xiàn)在我們想要實(shí)現(xiàn)官網(wǎng) demo 中的 shuffle 效果,有了追加圖片邏輯的鋪墊,是不是已經(jīng)覺得思路如泉涌了?沒錯(cuò),即使圖片被打亂的再厲害,只要我們有「圖片開始時(shí)的位置」和「圖片結(jié)束時(shí)的位置」,那就可以輕松做到路徑動(dòng)畫。
現(xiàn)在我們需要做的是把動(dòng)畫的邏輯抽離出來(lái),我們分析一下整條鏈路:
保存舊位置 -> 改變數(shù)據(jù)驅(qū)動(dòng)視圖更新 -> 獲得新位置 -> 利用 FLIP 做動(dòng)畫
其實(shí)外部只需要傳入一個(gè) update 方法告訴我們?nèi)绾稳ジ聢D片數(shù)組,就可以把這個(gè)邏輯完全抽象到一個(gè)函數(shù)里去。
scheduleAnimation(update) {// 獲取舊圖片的位置const prevImgs = this.$refs.imgs.slice()const prevSrcRectMap = createSrcRectMap(prevImgs)// 更新數(shù)據(jù)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: 300,easing: "cubic-bezier(0,0,0.32,1)",}const animation = currentRect.img.animate(keyframes, options)})}) }那么追加圖片和亂序的函數(shù)就變得非常簡(jiǎn)單了:
// 追加圖片 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)}) }源碼地址
https://github.com/sl1673495/flip-animation
總結(jié)
FLIP
FLIP 不光可以做位置變化的動(dòng)畫,對(duì)于透明度、寬高等等也一樣可以很輕松的實(shí)現(xiàn)。
比如電商平臺(tái)中經(jīng)常會(huì)出現(xiàn)一個(gè)動(dòng)畫,點(diǎn)擊一張商品圖片后,商品從它本來(lái)的位置慢慢的放大成了一張完整的頁(yè)面。
FLIP的思路掌握后,只要你知道元素動(dòng)畫前的狀態(tài)和元素動(dòng)畫后的狀態(tài),你都可以輕松的通過「倒置狀態(tài)」后,讓它們做一個(gè)流暢的動(dòng)畫后到達(dá)目的地,并且此時(shí)的 DOM 狀態(tài)是很干凈的,而不是通過大量計(jì)算的方式強(qiáng)迫它從 0, 0 位移到 100, 100,并且讓 DOM 樣式上留下 transform: translate(100px, 100px) 類似的字樣。
Web Animation
利用 Web Animation API 可以讓我們用 JavaScript 更加直觀的描述我們需要元素去做的動(dòng)畫,想象一下這個(gè)需求如果用 CSS 來(lái)做,我們大概會(huì)這樣去完成這個(gè)需求:
const currentImgStyle = currentRect.img.style currentImgStyle.transform = `translate(${invert.left}px, ${invert.top}px)` currentImgStyle.transitionDuration = "0s"this._reflow = document.body.offsetHeightcurrentRect.img.classList.add("move")currentImgStyle.transform = currentRect.img.style.transitionDuration = ""currentRect.img.addEventListener("transitionend", () => {currentRect.img.classList.remove("move") })這是選擇用比較原生的方式去控制 CSS 樣式實(shí)現(xiàn)的 FLIP 動(dòng)畫,這段代碼讓我覺得不舒服的點(diǎn)在于:
而利用 Web Animation API 的代碼則變得非常符合直覺和易于維護(hù):
const keyframes = [{transform: `translate(${invert.left}px, ${invert.top}px)`,},{ transform: "" }, ] const options = {duration: 300,easing: "cubic-bezier(0,0,0.32,1)", }const animation = currentRect.img.animate(keyframes, options)關(guān)于兼容性問題,W3C 已經(jīng)提供了 Web Animation API Polyfill,可以放心大膽的使用。
期待在不久的未來(lái),我們可以拋棄舊的動(dòng)畫模式,迎接這種更新更好的 API。
希望這篇文章能讓對(duì)動(dòng)畫發(fā)愁的你有一些收獲,謝謝!
與50位技術(shù)專家面對(duì)面20年技術(shù)見證,附贈(zèng)技術(shù)全景圖總結(jié)
以上是生活随笔為你收集整理的callback回调使用 vue_前端动画必知必会:React 和 Vue 都在用的 FLIP 思想实战的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 《出发吧麦芬》熔炼装备方法
- 下一篇: iis vue history 配置_V