1. 程式人生 > >深入理解requestAnimationFrame並實現相簿元件中的切換動畫

深入理解requestAnimationFrame並實現相簿元件中的切換動畫

全手打原創,轉載請標明出處:[https://www.cnblogs.com/dreamsqin/p/12529885.html](https://www.cnblogs.com/dreamsqin/p/12529885.html),多謝,=。=~ (如果對你有幫助的話請幫我點個贊啦) > 通常情況下,我們利用HTML5的canvas,CSS3的transform、transition、animation實現動畫效果,但是今天為了實現相簿元件中`scrollLeft`改變的動效,怎麼用js實現動畫還不影響效果和效能~=。=,居然讓我發現了一個神奇的存在:`requestAnimationFrame`,下面來深入學習一下。 ## 效果展示 ***動畫的本質就是要讓人眼看到影象被重新整理而引起變化的視覺效果,而這個變化要以連貫的、平滑的方式進行過渡。*** 那如何從原理上實現這種效果呢?或者說怎麼讓改變顯得不會那麼突兀?在說明前需要先科普一下相關的小知識。 首先看一下最終實現的效果 ↓ ##### 使用動畫效果前:閃現式移動 ![](https://img2020.cnblogs.com/blog/1074523/202003/1074523-20200320094706797-2019277859.gif) ##### 使用動畫效果後:平滑式過渡 ![](https://img2020.cnblogs.com/blog/1074523/202003/1074523-20200320094732053-1425270645.gif) ## 科普小知識 #### 1、螢幕重新整理(繪製)頻率 ***指影象在螢幕上更新的速度,也就是螢幕上的影象每秒鐘出現的次數,單位為赫茲(Hz)。*** 對於一般膝上型電腦,這個頻率大概是60Hz, 可以在桌面上`右鍵 > 螢幕解析度 > 高階設定 > 監視器 > 螢幕重新整理頻率`中檢視和設定。這個值的設定受螢幕解析度、螢幕尺寸和顯示卡的影響,原則上設定成讓眼睛看著舒適的值就可以了。 ![](https://img2020.cnblogs.com/blog/1074523/202003/1074523-20200320094826181-317396861.png) **常見的兩種顯示器**: **CRT**: 一種使用陰極射線管(Cathode Ray Tube)的顯示器。螢幕上的圖形影象是由一個個熒光點(因電子束擊打而發光)組成,由於映象管內熒光粉受到電子束擊打後發光的時間很短,所以電子束必須不斷擊打熒光粉使其持續發光。`電子束每秒擊打熒光粉的次數就是螢幕重新整理頻率`。 **LCD**: 我們常說的液晶顯示器( Liquid Crystal Display)。因為 LCD中每個畫素在背光板的作用下都在持續不斷地發光,直到不發光的電壓改變並被送到控制器中,所以 LCD 不會有電子束擊打熒光粉而引起的閃爍現象。 因此,當你對著電腦螢幕什麼也不做的情況下,顯示器也會以每秒60次的頻率不斷更新螢幕上的影象。 **為什麼你感覺不到這個變化?** 那是因為人的眼睛有`視覺暫留`,即前一副畫面留在大腦的印象還沒消失,緊接著後一副畫面就跟上來了,這中間只間隔了16.7ms(1000/60≈16.7), 所以會讓你誤以為螢幕上的影象是靜止不動的。 而螢幕給你的這種感覺是對的,試想一下,如果重新整理頻率變成1Hz(1次/秒),螢幕上的影象就會出現嚴重的閃爍,這樣很容易引起眼睛疲勞、痠痛和頭暈目眩等症狀。 #### 2、動畫原理 根據`螢幕重新整理頻率`我們知道,你眼前所看到影象正在以每秒 60 次的頻率繪製,由於頻率很高,所以你感覺不到它的變化。 60Hz 的螢幕每 16.7ms 繪製一次,如果在螢幕每次繪製前,將元素的位置向右移動一個畫素,即`Px += 1`,這樣一來,螢幕每次繪製出來的影象位置都比前一個差1px,你就會看到影象在移動。 由於人眼的`視覺暫留`,***當前位置的影象停留在大腦的印象還沒消失,緊接著影象又被移到了下一個位置,所以你所看到的效果就是影象在流暢的移動。***這就是視覺效果上形成的動畫。 感受一下↓(因為`視覺暫留`,讓你感覺這個人在走動) ![](https://img2020.cnblogs.com/blog/1074523/202003/1074523-20200320094935032-707212203.gif) #### 3、Element.scrollLeft 要實現相簿元件中照片的移動需要使用dom元素的一個很重要的屬性`scrollLeft`,***可以讀取或設定元素滾動條到元素左邊的距離。***聽上去好像有點兒繞,畫個圖看看: ![](https://img2020.cnblogs.com/blog/1074523/202003/1074523-20200320095025386-205415803.png) 藍色的是元素的滾動條,`scrollLeft`則是紅段標註的滾動條到元素左邊的距離,後續相簿中圖片的切換需要通過修改`scrollLeft`值實現。 ## setTimeout實現動畫 瞭解了動畫原理後,假定在`requestAnimationFrame`出現以前,在JavaScript 中想要實現上述動畫效果,怎麼辦呢?無外乎就是用`setTimeout`或`setInterval`,***通過設定一個間隔時間來不斷改變影象的位置,從而達到動畫效果***,本文以`setTimeout`為例。 ```javascript // demo1: function moveTo(dom, to) { dom.scrollLeft += 1; if(dom.scrollLeft <= to) { setTimeout(() => { moveTo(dom, to) }, 16.7) } } ``` 但我們會發現,利用`setTimeout`實現的動畫在某些低端機上會出現卡頓、抖動的現象。 **這種現象的產生有兩個原因:** > **`setTimeout`的執行時間並不是確定的。**在Javascript中, `setTimeout `任務被放進了非同步佇列中,只有當主執行緒上的任務執行完以後,才會去檢查該佇列裡的任務是否需要開始執行,因此`setTimeout `的實際執行時間一般要比其設定的時間晚一些。 **重新整理頻率受螢幕解析度和螢幕尺寸的影響。**因此不同裝置的螢幕重新整理頻率可能會不同,而`setTimeout`只能設定一個固定的時間間隔,這個時間不一定和螢幕的重新整理時間相同。 以上兩種情況都會導致`setTimeout`的執行步調和螢幕的重新整理步調不一致,從而引起丟幀現象。 **那為什麼步調不一致就會引起丟幀呢?** 首先要明白,`setTimeout`的執行只是在記憶體中對影象屬性進行改變,這個變化必須要等到螢幕下次重新整理時才會被更新到螢幕上。如果兩者的步調不一致,就可能會導致中間某一幀的操作被跳過去,直接更新下一幀的影象。 **舉個栗子~** 假設螢幕每隔16.7ms重新整理一次,而`setTimeout`每隔10ms設定影象向右移動1px, 就會出現如下繪製過程: ![](https://img2020.cnblogs.com/blog/1074523/202003/1074523-20200320095049988-1883164465.png) 從上面的繪製過程中可以看出,螢幕沒有更新`left = 2px`的那一幀畫面,影象直接從1px的位置跳到了3px的的位置,這就是丟幀現象,會引起動畫卡頓。而原因就是`setTimeout`的執行步調和螢幕的重新整理步調不一致。 開發者可以用很多方式來減輕這些問題的症狀,但徹底解決基本很難,**問題的根源在於時機**: >**對於前端開發者來說**,`setTimeout`提供的是一個等長的定時器迴圈(timer loop),我們對於瀏覽器核心對渲染函式的響應以及何時能夠發起下一個動畫幀的時機,是完全不瞭解的。 **對於瀏覽器核心來說**,它能夠了解發起下一個渲染幀的合適時機,但是對於任何 `setTimeout`傳入的回撥函式執行,都是一視同仁的。它很難知道哪個回撥函式是用於動畫渲染的,因此,優化的時機非常難以掌握。 總的來說就是,寫 JavaScript 的人瞭解一幀動畫在哪行程式碼開始,哪行程式碼結束,卻不瞭解應該何時開始,應該何時結束,而在核心引擎來說,卻恰恰相反,所以二者很難完美配合,直到 `requestAnimationFrame`出現。 ## requestAnimationFrame實現動畫 與`setTimeout`相比,`requestAnimationFrame`***最大的優勢是由瀏覽器來決定回撥函式的執行時機,即緊跟瀏覽器的重新整理步調。*** 具體一點講,如果螢幕重新整理頻率是60Hz,那麼回撥函式每16.7ms被執行一次,如果螢幕重新整理頻率是75Hz,那麼這個時間間隔就變成了1000/75=13.3ms。它能保證回撥函式在螢幕每一次的重新整理間隔中只被執行一次,這樣就不會引起丟幀現象,自然不會導致動畫的卡頓。 ```javascript // demo2: function moveTo(dom, to) { dom.scrollLeft += 1; if(dom.scrollLeft <= to) { window.requestAnimationFrame(() => { moveTo(element, to) }) } } ``` 除此之外,`requestAnimationFrame`還有***以下兩個優勢***: >**CPU節能:**使用`setTimeout`實現的動畫,當頁面被隱藏(