requestAnimationFrame,不止是动画

56 阅读2分钟

window.requestAnimationFrame() 告诉浏览器——你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行,当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数 (即你的回调函数)。

注意: 若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 window.requestAnimationFrame()

特点

  • requestAnimationFrame会把每一帧中的所有DOM操作集中起来,在一次重绘或回流中就完成,并且重绘或回流的时间间隔紧紧跟随浏览器的刷新频率。如果系统绘制率是 60Hz,那么回调函数就会16.7ms再 被执行一次,如果绘制频率是75Hz,那么这个间隔时间就变成了 1000/75=13.3ms。换句话说就是,requestAnimationFrame的执行步伐跟着系统的绘制频率走。它能保证回调函数在屏幕每一次的绘制间隔中只被执行一次,这样就不会引起丢帧现象,也不会导致动画出现卡顿的问题。
  • requestAnimationFrame() 运行在后台标签页或者隐藏的iframe 里时,requestAnimationFrame() 会被暂停调用以提升性能和电池寿命。

参数

  • callback

    下一次重绘之前更新动画帧所调用的函数 (即上面所说的回调函数)。该回调函数会被传入DOMHighResTimeStamp参数,该参数与performance.now()的返回值相同,它表示requestAnimationFrame() 开始去执行回调函数的时刻。

返回值

一个 long 整数,请求 ID,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。

应用场景

平滑滚动到页面顶部

<!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>平滑滚动到页面顶部</title>
    <style>
      body {
        display: flex;
        flex-direction: column;
        align-items: center;
      }
      .main {
        width: 200px;
        height: 200vh;
        margin: 20px 0;
        border-radius: 20px;
        background-color: #eee;
        overflow-y: auto;
      }
    </style>
  </head>

  <body>
    <div>top</div>
    <div class="main"></div>
    <button onclick="scrollToTop()">scrollToTop</button>
    <script>
      const scrollToTop = () => {
        const c = document.documentElement.scrollTop || document.body.scrollTop
        if (c > 0) {
          window.requestAnimationFrame(scrollToTop)
          window.scrollTo(0, c - c / 8)
        }
      }
    </script>
  </body>

</html>

大量数据渲染

<!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>大量数据渲染</title>
  </head>

  <body>
    <div id="container"></div>
    <script>
      //需要插入的容器
      let ul = document.getElementById('container')
      // 插入十万条数据
      let total = 1000
      // 一次插入 20 条
      let once = 2
      //总页数
      let page = total / once
      //每条记录的索引
      let index = 0
      //循环加载数据
      function loop (curTotal, curIndex) {
        if (curTotal <= 0) {
          return false
        }
        //每页多少条
        let pageCount = Math.min(curTotal, once)
        window.requestAnimationFrame(function () {
          for (let i = 0; i < pageCount; i++) {
            let li = document.createElement('li')
            li.innerText = curIndex + i + ' : ' + ~~(Math.random() * total)
            ul.appendChild(li)
          }
          loop(curTotal - pageCount, curIndex + pageCount)
        })
      }

      loop(total, index)

    </script>
  </body>

</html>

进度条

<!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>Document</title>
    <style>
      .progress-wrap {
        position: relative;
        width: 300px;
        height: 32px;
        margin: 100px;
        background-color: #eee;
        border-radius: 20px;
        line-height: 32px;
        text-align: center;
      }

      .progress-bar {
        position: absolute;
        top: 0;
        left: 0;
        background-color: aquamarine;
        border-radius: 20px;
      }

      .progress-num {
        position: relative;
        z-index: 9;
      }
    </style>
  </head>

  <body>

    <body>

      <div class="progress-wrap">
        <div class="progress-bar" style="width: 0px;height:32px;"></div>
        <span class="progress-num">0%</span>
      </div>

      <script>
        const progressWrap= document.querySelector('.progress-wrap')
        const progressBar = document.querySelector('.progress-bar')
        const progressNum = document.querySelector('.progress-num')

        progressWrap.onclick = function () {

          let timer = requestAnimationFrame(function fn () {
            if (parseInt(progressBar.style.width) < 300) {
              progressBar.style.width = parseInt(progressBar.style.width) + 3 + 'px';
              progressNum.innerHTML = parseInt(progressBar.style.width) / 3 + '%'
              timer = requestAnimationFrame(fn)
            } else {
              cancelAnimationFrame(timer)
            }
          })

        }

      </script>
    </body>

  </body>

</html>

定时执行

参考 模拟setTimeout/setInterval

模拟setTimeout/setInterval

<!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>模拟 setTimeout-setInterval</title>
    <style>
      .btn {
        width: 100px;
        height: 40px;
        margin-bottom: 20px;
        background-color: #eee;
        border-radius: 20px;
        text-align: center;
        line-height: 40px;
      }
    </style>
  </head>

  <body>
    <div class="btn setTimeout"> setTimeout</div>
    <div class="btn setInterval">setInterval</div>

    <script>

      function tick (options) {

        let lastTime = 0
        let rafId = 0
        let callBackNum = 0

        const step = (timestamp) => {
          if (timestamp - lastTime > options.interval) {
            options.cb()
            lastTime = timestamp
            callBackNum ++
          }
          if (options.type !== 'setTimeout' || callBackNum < 1 ) {
            window.requestAnimationFrame(step)
          }
        }

        const start = () => {
          lastTime = performance.now()
          window.requestAnimationFrame(step)
        }

        start()
      }

      function mySetTimeout () {
        console.log('mySetTimeout')
        tick({
          type: 'setTimeout',
          interval: 1000, 
          cb: () => {
            console.log('就 一次')
          }
        })
      }

      function mySetInterval () {
        console.log('mySetInterval')
        tick({
          type: 'setInterval',
          interval: 1000, 
          cb: () => {
            console.log(`1s 一次`)
          }
        })
      }

      document.querySelector('.setTimeout').addEventListener('click', () => {
        mySetTimeout()
      })

      document.querySelector('.setInterval').addEventListener('click', () => {
        mySetInterval()
      })
    </script>
  </body>

</html>

参考

1、 关于requestAnimationFrame的研究笔记

2、 JS 动画基础: 细说 requestAnimationFrame

3、使用requestAnimationFrame停止并继续动画

4、setTimeout vs requestAnimationFrame

5、【前端动画】requestAnimationFrame方法与动画

6、requestAnimationFrame 执行机制探索