关于JavaScript实现动画时动画抖动的原因与解决方法

目录
  • 使用定时器实现动画出现卡顿的原因
  • requestAnimationFrame 的前世今生
  • requestAnimationFrame VS setInterval
  • 参考资料
  • 总结

前段时间在使用 JavaScript 做动画的时候发现做出来的动画会出现卡顿的现象,今天我们主要就来聊一下卡顿的原因以及如何解决这个问题。

使用定时器实现动画出现卡顿的原因

  • 主要原因是浏览器无法确定定时器的回调函数的执行时机。以 setInterval 为例,当一个 setInterval 定时器被创建后,它的回调任务会被放到异步队列,只有当同步任务执行完成后,浏览器才会检查异步队列中是否有需要执行的异步任务,如果有,就取出执行,这样会使任务的实际执行时机比所设定的延迟时间要晚一些。

这个问题跟浏览器的事件循环机制有关,JavaScript 引擎在解析执行我们的代码的时候,遇到定时器,会调用浏览器 API,让定时器去进行倒计时,此时并不阻塞同步代码的执行,当定时器倒计时完毕,定时器回调会被放入宏任务队列等待执行。

在这个过程中问题就来了,如果说同步代码的执行需要50ms,而定时器设置的定时只有20ms,那么由于事件循环的机制,还是要等待同步任务执行完整之后再来执行微任务队列中的定时器回调,而这中间,又相隔了30ms,在这30ms的过程中,定时器的回调一直处于 pendding 的状态。如果定时器中是动画相关的操作,那也需要在预期的时间上多等待50ms。

画了张图,希望能帮助大家理解(如果不能帮助大家理解,那么请忽略这张图……)

  • 屏幕分辨率和尺寸也会影响刷新频率,不同设备的屏幕绘制频率可能会有所不同,而 setInterval 只能设置一个固定的时间间隔,这个间隔时间不一定与屏幕的刷新时间同步,所以就可能会导致动画出现随机丢帧的问题。

这里有两个点,一个是显示器的刷新频率,另一个是定时器的时间间隔。

一般显示器刷新频率都是60Hz,这基本上意味着每秒需要重绘60次。大多数浏览器都会限制重绘的频率,使其不超过显示器的刷新频率。因为超过刷新频率,用户也感知不到,白白浪费性能。

因此,实现平滑动画最佳的重绘间隔为1000ms/60,大约17毫秒。以这个速度重绘,可以实现最平滑的动画效果。因为这已经是浏览器的重绘频率的极限了。

知道何时绘制下一帧,是创造平滑动画的关键。直到几年前,都没有确切保证让浏览器在何时把下一帧绘制出来的方法。随着 <canvas>HTML5 游戏的兴起,开发者发现 setIntervalsetTimeout 的不精确是个大问题,而浏览器自身的计时器也存在着精度不足毫秒的问题。

以下是几个浏览器计时器的精度情况:

  • IE8 以及之前的版本计时器精度为 15.625ms;
  • IE9 及之后的版计时器精度为 4ms;
  • FireFox 和 Safari 的计时器精度约为 10ms;
  • Chrome 的计时器精度为 4ms。

以 Chrome 为例,它的计时器精度为 4ms,这意味着 0~4 之间的任何值最终要么是 0,要么是4;不可能是别的值。因此,即使将浏览器的时间间隔设置为最优,也免不了只能得到相近似的结果。

对于 JavaScript 来说,它不知道浏览器会在何时发生重绘。因此,我们通过定时器做动画的时候,在定时器中控制动画的代码已经执行完成的情况下,动画效果并不一定会立马生效,因为此时浏览器可能还处在等待下一次重绘的过程中,当下一次重绘完成,动画效果才能在浏览器窗口中显示出来。

而由于浏览器计时器时间差的问题,会导致定时器的计时并不一定是我们设置的 17 ms,而是在多个时间点内反复横跳,也因此才出现使用定时器做动画的时候动画抖动的问题,在复杂的动画中,这种问题尤为明显。

在这样的环境下,今天的主角 requestAnimationFrame 应运而生!

requestAnimationFrame 的前世今生

Mozilla 的 Robert O'Callahan 一直在思考这个问题,并且提出了一个独特的解决方案。他指出,浏览器知道 CSS 过渡和动画应该什么时候开始,并且能够计算出正确的时间间隔,到时间就去刷新用户界面。

但是对于 JavaScript 而言,浏览器并不知道动画什么时候开始。他给出的方案是创造一个名为 MozRequestAnimationFrame 的新方法,以此来通知浏览器某些 JavaScript 代码要执行动画了。这样浏览器就可以在运行某些代码后进行适当的优化。

目前,所有的浏览器都支持这个方法不带前缀的版本,也就是现在用到的 requestAnimationFrame

requestAnimationFrame VS setInterval

这里就不再过多的介绍 requestAnimationFrame 的详细用法了,它的用法并不复杂。

与定时器不同的是,requestAnimationFrame 只会在被调用的时候执行一次动画,而不会连续执行。如果想做连续的动画,则可以通过递归来实现对 requestAnimationFrame 的连续调用。

接下来通过一个 demo 来对比一下使用 requestAnimationFramesetInterval 两者做出来的动画效果之间的差异。

<!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>
  </head>
  <style>
    .square1,
    .square2 {
      position: absolute;
      width: 100px;
      height: 100px;
    }
    .square1 {
      top: 40px;
      background: red;
    }
    .square2 {
      top: 150px;
      background: blue;
    }
  </style>
  <body>
    <div class="container">
      <div class="square1"></div>
      <div class="square2"></div>
      <button class="btn">开始!</button>
    </div>
    <script>
      const square1El = document.querySelector('.square1')
      const square2El = document.querySelector('.square2')
      // 定时器版
      function squareMove() {
        let timer = null
        square1El.style.left = '0px'
        timer = setInterval(() => {
          const squareLeft = parseInt(square1El.style.left)
          if (squareLeft >= 500) return clearInterval(timer)
          square1El.style.left = squareLeft + 1 + 'px'
        }, 17)
      }

      // requestAnimationFrame 版
      function squareMove2() {
        let timer = null
        square2El.style.left = '0px'

        function updateAnimation() {
          const squareLeft = parseInt(square2El.style.left)
          if (squareLeft >= 500) return cancelAnimationFrame(timer)
          square2El.style.left = squareLeft + 1 + 'px'
          window.requestAnimationFrame(updateAnimation)
        }

        window.requestAnimationFrame(updateAnimation)
      }

      document.querySelector('.btn').addEventListener('click', () => {
        squareMove()
        squareMove2()
      })
    </script>
  </body>
</html>

在页面中画了两个正方形,当点击按钮的时候方块开始运动,红色方块是使用 setInterval 实现的动画,蓝色方块使用的是 requestAnimationFrame

接下来看一下实现的效果。

在生成gif的时候视频被压缩了,但是还是能看到红色的方块在开始运动的时候有明显的抖动,而蓝色的方块则比较丝滑。

实际上,requestAnimationFrame 的回调函数可以接收一个参数,这个参数是一个 DOMHightResTimeStamp 的实例(比如:performance.now()的返回值),用来表示下一次重绘的时间。这一点非常重要,requestAnimationFrame 实际上是把重绘任务安排在了未来的一个已知的时间点上,而且通过这个参数来告诉开发者。

类似于 setInterval 的清除方法 clearIntervalrequestAnimationFrame 也有对应的取消重绘的方法 cancelAnimationFrame,用法也跟 clearInterval 非常类似,在每次调用 requestAnimationFrame 的时候,都会返回一个id,cancelAnimationFrame 就是通过这个id去取消对应的 requestAnimationFrame

看到这里,大家应该对 setIntervalrequestAnimationFrame 都有了更深的了解,以后使用 JavaScript 做动画还是以 requestAnimationFrame 为主。

希望讲解的内容能对大家有所帮助~

参考资料

[1]《JavaScript高级程序设计(第四版)》第18章第1节。

总结

到此这篇关于JavaScript实现动画时动画抖动的原因与解决方法的文章就介绍到这了,更多相关JS动画抖动内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • JS实现简单抖动效果

    废话不多说了,直接给大家贴代码了,具体代码如下所示: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <style> #div1 { width: 100px; height: 100px; position: absolute; left: 400px; top:

  • js 窗口抖动示例

    复制代码 代码如下: <html> <head> <title> New Document </title> <meta name="Generator" content="EditPlus"> <meta name="Author" content=""> <meta name="Keywords" content="

  • js 鼠标放图片上抖动效果

    在线演示: 鼠标移至图片后抖动的JS代码 .shakeimage{position:relative; left:100px; top:100px;} var typ=["marginTop","marginLeft"],rangeN=10,timeout=20; function shake(o,end){ var range=Math.floor(Math.random()*rangeN); var typN=Math.floor(Math.random()*t

  • 通过实例讲解JS如何防抖动

    前言 这道题目经常与事件触发器同时存在,为了考察面试者在一些具体业务流程上(信息流,搜索框输入查询)等,能否综合的考虑实现思路. 题目 在某些信息列表中一般采用瀑布流,滚动一屏时加载相应的数据,请思考如何避免连续下拉时而产生的问题(可能是页面崩溃,也可能是巨卡无比). 一般情况下,如果碰到这样的面试题,防抖动机制,就能很好的解决,这方面最早的应用实践还是Twitter,开发者写了一篇博客,详细的阐述了如何解决这样的问题.那么,说到防抖动,其核心内涵在于延迟处理,也就是将一系列的事件处理程序全部延

  • JS实现仿QQ聊天窗口抖动特效

    JS实现仿QQ聊天窗口抖动特效 <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=gb2312" /> <title>JavaScript层抖动效果</title> <style type="text/css"> #body{text-align:center;} #test{w

  • 原生js实现类似弹窗抖动效果

    先在之前做的抖动窗口上做了点动作 无限变色 <!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Document</title> <style> div{text-align: center;line-height: 150px;font-weight: bold;} #dv{width: 300

  • JavaScript实现窗口抖动效果

    原理介绍 抖动其实是往复运动的一种特殊形式,只不过往复运动是一种无摩擦力的无限运动,且以速度为参照依据:而抖动以位置作为参照依据,最终停在起始点 在网页中最常见的一种抖动效果应该是窗口抖动提示了 抖动元素从起始点开始,先向右移动最大距离len,然后移动到对称的左边位置:然后再向右移动稍微小一点的距离,再移动到对称的左边位置:以此循环,最终元素停止在起始点 代码实现 抖动在代码实现上,无非就是通过定时器,每隔一段时间让left或top值进行变化 在运动实现中,有两种距离变化的思路 思路一 div.

  • 关于JavaScript实现动画时动画抖动的原因与解决方法

    目录 使用定时器实现动画出现卡顿的原因 requestAnimationFrame 的前世今生 requestAnimationFrame VS setInterval 参考资料 总结 前段时间在使用 JavaScript 做动画的时候发现做出来的动画会出现卡顿的现象,今天我们主要就来聊一下卡顿的原因以及如何解决这个问题. 使用定时器实现动画出现卡顿的原因 主要原因是浏览器无法确定定时器的回调函数的执行时机.以 setInterval 为例,当一个 setInterval 定时器被创建后,它的回

  • Ubuntu安装telent服务器时出现:apt-get:Package has no installation的原因及解决方法

    当我在终端敲下这条命令的时候,系统就提示telnetd:apt-get:Package has no installation sudo apt-get install xinetd telnetd 刚开始我以为是没有这个包,后来查了一下资料发现,有这个软件,于是百度了一下才知道解决以上问题需要如下操作: # apt-get update # apt-get upgrade # apt-get install <packagename> 这样就可以正常使用apt-get了. 接下来就按照以前转

  • android真机调试时无法显示logcat信息的解决方法介绍

    android真机调试时无法显示logcat信息的解决方法介绍: window-->show view-->android->devices, 打开devices,点击右边的截屏图片的按钮.等到出现截图的时候,logcat就出来信息了!

  • Android编程向服务器发送请求时出现中文乱码问题的解决方法

    本文实例讲述了Android编程向服务器发送请求时出现中文乱码问题的解决方法.分享给大家供大家参考,具体如下: 我们在andorid项目中通过get方式向服务器发送请求,其中url参数带有中文,将会产生乱码,乱码产生的原因有两种: 1. 在提交参数时,没有对中文参数进行URL编码 2. Tomcat服务器默认采用的是IOS8859-1编码(不支持中文)得到参数值 解决: 1. 进入android项目,在其中要提交参数的时候,对参数的值进行编码: 复制代码 代码如下: URLEncoder.enc

  • fastjson生成json时Null属性不显示的解决方法

    举个例子 Map < String , Object > jsonMap = new HashMap< String , Object>(); jsonMap.put("a",1); jsonMap.put("b",""); jsonMap.put("c",null); jsonMap.put("d","wuzhuti.cn"); String str = JSO

  • Android中利用NetworkInfo判断网络状态时出现空指针(NullPointerException)问题的解决方法

    在Android中,很多人会用如下的方法判断当前网络是否可用: /** * 获取当前网络状态(是否可用) */ public static boolean isNetworkAvailable() { boolean isAalable = false; ConnectivityManager connManager = (ConnectivityManager) BaseApplication.getApplication().getSystemService(Context.CONNECTI

  • Android程序启动时出现黑屏问题的解决方法

    本文实例讲述了Android程序启动时出现黑屏问题的解决方法.分享给大家供大家参考,具体如下: 关于黑屏: 默认的情况下,程序启动时,会有一个黑屏的时期,原因是,首个activity会加载一些数据,比如初始化列表数据.向服务器发送请求获取数据等等. 去除方法: 1.在style里面添加一个style: <style name="ContentOverlay"parent="@android:style/Theme.Light"> <itemname

  • mysql5.7.18安装时mysql服务启动失败的解决方法

    MySQL 是一个非常强大的关系型数据库.但有些初学者在安装配置的时候,遇到种种的困难,在此就不说安装过程了,说一下配置过程.在官网下载的mysql时候,有msi格式和zip格式.Msi直接运行安装即可,zip则解压在自己喜欢的目录地址即可.在安装这两种的时候,都需要配置才能用.以下介绍主要是msi格式默认的地址:C:\Program Files\ mysql-5.7.18-win32. 一.在安装或者解压后,需要配置环境变量,过程如下:我的电脑->属性->高级系统设置->高级->

  • Android使用百度地图出现闪退及定位时显示蓝屏问题的解决方法

    使用百度地图出现闪退 一般情况下出现闪退是在AndroidManifest.xml文件中未在application标签中配置 <meta-data android:name="com.baidu.lbsapi.API_KEY" android:value="D9Lh8MrrLMUuXdWMU8tRLtDsta6PoaYN" /> 但是,有些同学会发现,所有配置都是按照官网或者教程上的步骤来配置依旧会出现闪退问题.此时,不要盲目直接去网上搜索使用百度地图出

  • linux安装软件时提示软件包不存在的解决方法

    软件包不存在时可能是被安装软件的软件源没有安装,我的是cent os,安装w3m时,出现这个问题,解决办法:先安装软件源(epel-release),然后更新系统包,最后执行安装软件操作,就可以成功了. 以安装w3m为例: 1.安装w3m的软件源:epel-release yum install epel-release //如果不是系统管理员需要前面加入sudo命令,因为安装需要管理员权限 2.安装成功后,更新系统包 (如果不是系统管理员需要前面加入sudo命令,因为安装需要管理员权限) yu

随机推荐