效能最佳化之通俗易懂學習requestAnimationFrame和使用場景舉例

水冗水孚 發表於 2023-01-24
一項新技術新的技術方案的提出,一定是為了解決某個問題的,或者是對某種方案的最佳化,比如window.requestAnimationFrame這個api...

requestAnimationFrame官方介紹

requestAnimationFrame用處概述

window.requestAnimationFrame() 告訴瀏覽器——你希望執行一個動畫,並且要求瀏覽器在下次重繪之前呼叫指定的回撥函式更新動畫。該方法需要傳入一個回撥函式作為引數,該回撥函式會在瀏覽器下一次重繪之前執行...

官方文件對應截圖

效能最佳化之通俗易懂學習requestAnimationFrame和使用場景舉例

官方文件:https://developer.mozilla.org...

大致看了以後,我們可以知道:

requestAnimationFrame這個api主要是用來做動畫的。

requestAnimationFrame這個api主要是用來做動畫的。

requestAnimationFrame這個api主要是用來做動畫的。

其實顧名思義,我們翻譯這個英文單詞,也能大致明白。request(請求)Animation(動畫)Frame(幀)

關於前端動畫的兩個問題:

1.前端動畫方案有哪些?

2.為何偏偏要使用這個新的api來做動畫(或者說這個api較之前做動畫的方式優點有哪些)?

1.前端動畫方案有哪些?

主要分類為css動畫js動畫,如下細分:

  • css動畫

    • transition過渡動畫
    • animation直接動畫(搭配@keyframes
  • js動畫

    • setIntervalsetTimeout定時器(比如不停地更改dom元素的位置,使其運動起來)
    • canvas動畫,搭配js中的定時器去運動起來(canvas只是一個畫筆,然後我們透過定時器會使用這個畫筆去畫畫-動畫)
    • requestAnimationFrame動畫(js動畫中的較好方案)
另有svg動畫標籤,不過工作中這種方式是比較少的,這裡不贅述

2.為何偏偏要使用這個新的api來做動畫(或者說這個api較之前做動畫的方式優點有哪些)?

在工作中,做動畫最優的方案無疑是css動畫,但是某些特定場景下,css動畫無法實現我們所需要的需求,此時,我們就要考慮使用js去做動畫了

canvas動畫本質也是定時器動畫

使用定時器動畫幹活,實際上是可以的,但是存在一個最大的問題,就是動畫會抖動動畫會抖動動畫會抖動,體驗效果不是非常好。

而,使用requestAnimationFrame去做動畫,就不會抖動就不會抖動就不會抖動

這裡筆者寫一個demo動畫(分別是上述兩種方式實現dom元素向右平移)給大家看一下,就知道具體的區別。我們先看一下效果圖:(紅色dom是定時器實現、綠色domrequestAnimationFrame實現)

效能最佳化之通俗易懂學習requestAnimationFrame和使用場景舉例

因為筆者的gif錄製軟體的問題,看著都有點卡,實際上,大家把下方程式碼複製一份跑起來看的話,會發現定時器動畫在微微顫抖,而requestAnimationFrame動畫卻穩如老狗
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>requestAnimationFrame_yyds</title>
    <style>
        body {
            box-sizing: border-box;
            background-color: #ccc;
        }

        .box1,
        .box2 {
            position: absolute;
            width: 160px;
            height: 160px;
            line-height: 160px;
            text-align: center;
            color: #fff;
            font-size: 13px;
        }

        .box1 {
            top: 40px;
            background: red;
        }

        .box2 {
            top: 210px;
            background: green;
        }
    </style>
    </style>
</head>

<body>
    <button class="btn">👉 let's go!</button>
    <div class="box1">定時器動畫</div>
    <div class="box2">請求動畫幀</div>
    <script>
        // 動畫思路:不斷修改dom元素的left值,使其運動起來(動畫)
        let box1 = document.querySelector('.box1')
        let box2 = document.querySelector('.box2')

        // setInterval定時器方式
        function setIntervalFn() {
            let timer = null
            box1.style.left = '0px'
            timer = setInterval(() => {
                let leftVal = parseInt(box1.style.left)
                if (leftVal >= 720) {
                    clearInterval(timer)
                } else {
                    box1.style.left = leftVal + 1 + 'px'
                }
            }, 17)
        }

        // requestAnimationFrame請求動畫幀方式
        function requestAnimationFrameFn() {
            let timer = null // 可注掉
            box2.style.left = '0px'
            function callbackFn() {
                let leftVal = parseInt(box2.style.left)
                if (leftVal >= 720) {
                    // 不再繼續遞迴呼叫即可,就不會繼續執行了,下面這個加不加都無所謂,因為影響不到
                    // cancelAnimationFrame取消請求動畫幀,用的極少,看下,下文中的回到頂部元件
                    // 大家會發現並沒有使用到這個api(這樣寫只是和clearInterval做一個對比)
                    // 畢竟,正常情況下,requestAnimationFrame會自動停下來
                    cancelAnimationFrame(timer) // 可注掉(很少用到)
                } else {
                    box2.style.left = leftVal + 1 + 'px'
                    window.requestAnimationFrame(callbackFn)
                }
            }
            window.requestAnimationFrame(callbackFn)
        }

        // 動畫繫結
        let btn = document.querySelector('.btn')
        btn.addEventListener('click', () => {
            setIntervalFn()
            requestAnimationFrameFn()
        })
    </script>
</body>

</html>
Chrome瀏覽器檢視當前幀數命令:1. F12開啟控制檯2. command + shift + p調出輸入皮膚3. 在Run輸入框中輸入:Show frames per second(FPS) meter回車即可

透過上述的例子,我們可以回答這個問題了:

  • 面試官問:requestAnimationFrame比定時器好在哪裡?
  • 候選人答:好在比較穩定,動畫不卡頓
  • 面試官說:你回去等通知吧...

所以在這裡,我們還要順帶延伸一下,為什麼定時器會卡,而requestAnimationFrame不會卡。在說這個問題之前,這裡再提一下,requestAnimationFrame的語法規則

requestAnimationFrame的語法規則

一言以蔽之:requestAnimationFramejs中的setTimeout定時器函式基本一致,不過setTimeout可以自由設定間隔時間,而requestAnimationFrame的間隔時間是由瀏覽器自身決定的,大約是17毫秒左右

1.requestAnimationFrame我們可以在控制檯輸入window,然後展開檢視其身上的屬性,就能找到了,如下圖:

效能最佳化之通俗易懂學習requestAnimationFrame和使用場景舉例

2.由上圖我們可以看到,requestAnimationFrame本質上是一個全域性window物件上的一個屬性函式,函式是要被執行的,要被呼叫的。所以我們使時,直接:window.requestAnimationFrame(callBack)即可。

3.和定時器一樣其接收的引數callback也是一個函式,即下一次重繪之前更新動畫幀所呼叫的函式,即在這個函式體中,我們可以寫對應的邏輯程式碼(和定時器類似)

4.requestAnimationFrame也有返回值,返回值是一個整數,主要是定時器的身份證標識,可以使用`
window.cancelAnimationFrame()來取消回撥函式執行,相當於定時器中的clearTimeout()`。

5.二者也都是隻執行一次,想要繼續執行,做到類似setInterval的效果,需要寫成遞迴的形式(上述案例中也提到了)

為什麼定時器會卡,而requestAnimationFrame不會卡

為什麼定時器會卡

  • 我們在手機或者電腦螢幕上看東西時,螢幕會默默的不停地幹活(重新整理畫面)
  • 這個重新整理值得是每秒鐘重新整理次數,普通顯示器的重新整理率約為60Hz(每秒重新整理60次),高檔的有75Hz、90Hz、120Hz、144Hz等等
  • 重新整理率次數越高,顯示器顯示的影像越清晰、越流暢、越絲滑
  • 不重新整理就是靜態的畫面,重新整理比較低就是卡了PPT的感覺
  • 動畫想要絲滑流暢,需要卡住時間點進行程式碼操作(程式碼語句賦值、瀏覽器重繪)
  • 所以只需要每隔1000毫秒的60分之一(60HZ)即約為17毫秒,進行一次動畫操作即可
  • 只要卡住這個17毫秒,每隔17毫秒進行操作,就能確保動畫絲滑
  • 但是定時器的回撥函式,會受到js的事件佇列宏任務、微任務影響,可能設定的是17毫秒執行一次,但是實際上這次是17毫秒、下次21毫秒、再下次13毫秒執行,所以並不是嚴格的卡住了這個60HZ的時間
  • 沒有在合適的時間點操作,就會出現:類似這樣的情況:不變不變不變...
  • 於是就出現了,繪製不及時的情況,就會有抖動的出現(以上述案例,位置和時間沒有線性對應更新變化導致看起來抖動)

js執行程式碼是很快的,可能不到一毫秒,大家可以使用相應console的api去測試,如下:

console.time()
let box1 = document.querySelector('.box1')
box1.style.left = '100px'
console.timeEnd()

// js執行耗時結果:default: 0.044189453125 ms

為何requestAnimationFrame不會卡

requestAnimationFrame能夠做到,精準嚴格的卡住顯示器重新整理的時間,比如普通顯示器60HZ它會自動對應17ms執行一次,比如高階顯示器120HZ,它會自動對應9ms執行一次。

當然requestAnimationFrame只會執行一次,想要使其多次執行,要寫成遞迴的形式。上述案例也給出了遞迴寫法

至於為何requestAnimationFrame能夠卡住時間,其底層原理又是啥?本文暫且按下不表。

所以,這就是requestAnimationFrame的好處。

所以,上述內容驗證了:一項新技術新的技術方案的提出,一定是為了解決相關的問題的。

所以,window.requestAnimationFrame這個api就是解決了定時器不精準的問題的。

這就是其產生的原因。

requestAnimationFrame應用場景舉例-回到頂部元件

比如:回到頂部元件,就是使用requestAnimationFrame實現的。

下面是筆者封裝的回到頂部元件效果圖和程式碼

效果圖:

效能最佳化之通俗易懂學習requestAnimationFrame和使用場景舉例

也可以去筆者的網站上去看效果哦:http://ashuai.work:8888/#/myBack

程式碼:

<template>
  <transition name="fade-transform">
    <div
      v-show="visible"
      class="backWrap"
      :style="{
        bottom: bottom + 'px',
        right: right + 'px',
      }"
      @click="goToTop"
    >
      <slot></slot>
    </div>
  </transition>
</template>

<script>
export default {
  name: "myBack",
  props: {
    bottom: {
      type: Number,
      default: 72,
    },
    right: {
      type: Number,
      default: 72,
    },
    // 回到頂部出現的滾動高度位置
    showHeight: {
      type: Number,
      default: 240,
    },
    // 擁有捲軸的那個dom元素的id或者class,用於下方選中操作更改捲軸滾動距離
    scrollBarDom: String,
  },
  data() {
    return {
      visible: false,
      scrollDom: null,
    };
  },
  mounted() {
    if (document.querySelector(this.scrollBarDom)) {
      this.scrollDom = document.querySelector(this.scrollBarDom);
      // 不用給window繫結監聽滾動事件,給對應捲軸元素繫結即可
      this.scrollDom.addEventListener("scroll", this.isShowGoToTop, true);
    }
  },
  beforeDestroy() {
    // 最後要解除監聽滾動事件
    this.scrollDom.removeEventListener("scroll", this.isShowGoToTop, true);
  },
  methods: {
    isShowGoToTop() {
      // 獲取滾動的元素,即有捲軸的那個元素
      if (this.scrollDom.scrollTop > 20) {
        this.visible = true;
      } else {
        this.visible = false;
      }
    },
    goToTop() {
      // 獲取滾動的元素,即有捲軸的那個元素
      let scrollDom = document.querySelector(this.scrollBarDom);
      // 獲取垂直滾動的距離,看看滾動了多少了,然後不斷地修改滾動距離直至為0
      let scrollDistance = scrollDom.scrollTop;

      /**
       * window.requestAnimationFrame相容性已經可以了,正常都有的
       * */
      if (window.requestAnimationFrame) {
        let fun = () => {
          scrollDom.scrollTop = scrollDistance -= 36;
          if (scrollDistance > 0) {
            window.requestAnimationFrame(fun); // 只執行一次,想多次執行需要再呼叫
          } else {
            scrollDom.scrollTop = 0;
          }
        };
        window.requestAnimationFrame(fun);
        return;
      }

      /**
       * 沒有requestAnimationFrame的話,就用定時器去更改捲軸距離,使之滾動
       * */
      let timer2 = setInterval(() => {
        scrollDom.scrollTop = scrollDistance -= 36;
        if (scrollDistance <= 0) {
          clearInterval(timer2);
          scrollDom.scrollTop = 0;
        }
      }, 17);
    },
  },
};
</script>

<style lang='less' scoped>
.backWrap {
  position: fixed;
  cursor: pointer;
  width: 42px;
  height: 42px;
  background: #9cc2e5;
  border-radius: 4px;
  display: flex;
  justify-content: center;
  align-items: center;
  transition: all 0.5s;
}

// 過渡效果
.fade-transform-leave-active,
.fade-transform-enter-active {
  transition: all 0.36s;
}

.fade-transform-enter {
  opacity: 0;
  transform: translateY(-5px);
}
.fade-transform-leave-to {
  opacity: 0;
  transform: translateY(5px);
}
</style>
GitHub倉庫地址:https://github.com/shuirongsh...

類比學習reduce迴圈解決了forEach迴圈可能需要一個初始變數的問題

我們類比一下學習,比如既然有了forEach迴圈,為啥還又新推出一個reduce迴圈呢?

原因:某些場景下,reduce迴圈解決了forEach迴圈還需要再定義一個變數的問題。

似曾相識的感覺...

比如我們有一個需求,給一個陣列求和。

forEach寫法

let arr = [1, 3, 5, 7, 9]
function forEachFn(params) {
    let total = 0
    params.forEach((num) => {
        total = total + num
    })
    return total
}
let res1 = forEachFn(arr)
console.log(res1);

reduce寫法

let arr = [1, 3, 5, 7, 9]
function reduceFn(params) {
    return params.reduce((temp, num) => {
        temp = temp + num
        return temp
    }, 0)
}
let res2 = reduceFn(arr)
console.log(res2);

透過上述兩段程式碼,我們可以看到,reduce函式比forEach少寫了一個total變數,千萬不要小看這少寫的東西,某些情況下,會節省很多的工作量呢!

一項新技術新的技術方案的提出,一定是為了解決某個問題的,或者是對某種方案的最佳化,比如xxx