requestAnimationFrame 解析

本文将给大家介绍一个使用 js 实现动画的利器,requestAnimationFrame,我们一般情况下,在 js 实现一个动画,一般是使用 setInterval 实现,不过通常使用这个方法实现动画的时候,细看能够看见一些抖动感

什么是 requestAnimationFrame

  1. requestAnimationFrame 是浏览器提供的一个方法,我们可以通过 requestAnimationFrame 来告诉浏览器我们需要执行一个动画,并且这个动画触发的时机是浏览器在下次重绘之前调用指定的回调函数更新动画
  2. 该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
  3. 当然也有一些需要注意的地方:若你想在浏览器下次重绘之前继续更新下一帧动画,那么回调函数自身必须再次调用 requestAnimationFrame()。因为 requestAnimationFrame() 是一次性的
  4. 在大多数遵循 W3C 建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配
  5. 而如果想要取消 requestAnimationFrame 执行,可以通过 cancelAnimationFrame 方法

为什么 setInterval 实现动画会有一些抖动感

  1. 这个原因可能有不少,但是主要的就是 浏览器渲染帧的不同步

  2. 为什么会不同步呢?主要原因是浏览器无法确定定时器的回调函数的执行时机

  3. 我们知道 setInterval 是一个异步的任务,只有当同步任务执行完成之后才会执行异步任务,假设我们设置的 setInterval 是 20ms 执行一次,那么如果同步代码执行的时间是 10ms,也就表示实际上 setInterval 执行的时候已经隔了 30ms 的时间了

  4. 这是一点,那我们在猜想一下,按照 60 帧率来计算的话,应该是 16.7ms 执行一次,就可以达到一个比较细腻的动画,那么如果我们将 setInterval 的时间设置为 16.7ms 可以解决这个问题吗,也是不行的,还是这个异步的问题,加上等待同步代码执行时间,这个时间一定是不精确的,何况本身浏览器的计时存在细小的误差

  5. 为了方便理解,我们还是回到这个开始设置 20ms 的时间,按照预期 60 帧渲染的话,那么不算其他干扰的情况,那么执行的时间线如下:

    在这里插入图片描述

  6. 可以看到,第一帧没有渲染,20ms的时候执行了 setInterval 代码了,但是就需要等待下一次的执行时机,也就第二帧,而如果假设是 10ms 的话,那么第一帧可以找到,但是在执行第二帧的时候,其实 setInterval 执行了三次,也就是说在第二帧本该执行第二次 setInterval 所设置的动画数值,变了第三次 setInterval 设置的动画数值,出现了一次跳跃,那自然看起来也会存在抖动感

  7. 正是因为由于这种不同步,就导致了一次执行渲染一针或者多帧,或者当前帧没有渲染,下一次执行的时候数值跳跃等等,从而导致动画存在抖动感

  8. 这个更加具体一点大家可以参考手绘动画翻页这种效果,如果翻到某一页突然停一下,或者某两页或者几页黏在一起,被当做一页翻过去的时候,就可以名显的感觉到这个不连贯的感觉

  9. 或者有人说,我可以强制触发reflow来触发啊,那还是这个问题,屏幕的刷新率是固定的,你重绘时机过早也不会被展示出来,也不会被用户感知到,等于无效的操作,那么还是无法解决这个问题

使用 requestAnimationFrame

  1. 相信在经过上面 setInterval 实现动画为什么存在抖动感的解析,就知道应该要怎么解决,解决方案就是在每一帧渲染之前,就处理好每一帧需要表现的动画样式和一些数据,然后就可以在每一帧时机渲染的时候处理正确的动画效果,这样就可以保证我们的动画拥有相对细腻的效果了

  2. 而 requestAnimationFrame 就可以帮助我们完成这个效果

  3. requestAnimationFrame 是只会执行一次的,所以我们如果需要利用 requestAnimationFrame 实现动画效果的话,往往需要递归,也就是如下的形式,如下:

    function run(){
        requestAnimationFrame(run)
    }
    requestAnimationFrame(run)
    

requestAnimationFrame 对比 setInterval

  1. 实现代码:

    <!DOCTYPE html>
    <html lang="en">
    
    <head>
      <meta charset="UTF-8">
      <meta name="viewport" content="width=device-width, initial-scale=1.0">
      <title>requestAnimationFrame</title>
      <style>
        * {
          margin: 0;
          padding: 0;
          box-sizing: border-box;
        }
    
        .sign {
          width: 500px;
          height: 220px;
          border-right: 2px solid #000;
          position: relative;
          margin-left: 20px;
        }
    
        .box {
          width: 100px;
          height: 100px;
          background-color: salmon;
          position: absolute;
          left: 0;
          top: 0;
        }
    
        .box1 {
          top: 120px;
          background-color: skyblue;
          word-wrap: break-word;
        }
    
        .btns {
          margin-left: 20px;
          margin-top: 30px;
        }
      </style>
    </head>
    
    <body>
      <div class="sign">
        <div class="box">setInterval</div>
        <div class="box box1">requestAnimationFrame</div>
      </div>
      <div class="btns">
        <button class="s-btn">setInterval</button>
        <button class="r-btn">requestAnimationFrame</button>
        <button class="btn">一起执行</button>
      </div>
    
      <script>
        const box = document.querySelector('.box');
        const box1 = document.querySelector('.box.box1');
        const sBtn = document.querySelector('.s-btn')
        const rBtn = document.querySelector('.r-btn')
        const btn = document.querySelector('.btn')
    
        let boxLeft = 0
        let boxId = null
    
        let box1Left = 0
        let box1Id = null
    
    
        sBtn.addEventListener('click', () => {
          boxId = setInterval(() => {
            boxLeft++
            box.style.left = boxLeft + 'px'
            if (boxLeft >= 400) {
              clearInterval(boxId)
              boxId = null
            }
          }, 1000 / 60)
        })
    
        function run() {
          box1Left++
          box1.style.left = box1Left + 'px'
          if (box1Left >= 400) {
            cancelAnimationFrame(box1Id)
            box1Id = null
            return
          }
          requestAnimationFrame(run)
        }
    
        rBtn.addEventListener('click', () => {
          box1Id = requestAnimationFrame(run)
        })
    
        btn.addEventListener('click', () => {
          sBtn.click()
          rBtn.click()
        })
    
      </script>
    
    </body>
    
    </html>
    
  2. 屏幕刷新率为 60hz 时效果

    在这里插入图片描述

  3. 屏幕刷新率为 165hz 时

    在这里插入图片描述

  4. 由于这种 gif 的原因,可能实际感受不如在屏幕上看的明显,但是可以大致感知出来 setInterval 的那种抖动感,而且 setInterval 只能设置固定的时间,是无法契合当前的屏幕的刷新率的

  5. 这里在补充一点性能上的区别,这个就先了解一下他们在后台的运行机制:

    1. requestAnimationFrame() 运行在后台标签页或者隐藏的 <iframe> 里时,requestAnimationFrame()` 会被暂停调用以提升性能和电池寿命

    2. 而 setInterval 在后台也是不会停止调用的会继续在浏览器的内存中调用,消耗性能,比如最开始编写轮播图的时候,如果页面隔一段时间切换回来之后,会导致轮播图切换的非常快,就是因为没有停止执行,而切换回来之后,把失去的值都补上,这个也是可以验证的,比如我们把 setInterval 执行改为 2000ms,每次增加 50px,来看一下效果,如图

      在这里插入图片描述

    3. 通过这个是可以看到切换回来的时候,一下子就跳跃了一大段距离,就表示实际是没有停止执行的,一直改变着移动的数值,切换胡来的时候才会进行了跳跃式的移动

  6. 当然动画最优选还是 css3,不过有些动画总会需要 js 的介入嘛

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值