JS制作动画的三种方法

187 阅读3分钟

JS 中制作动画可以用以下三种 API

下面来介绍三种 API 是怎么工作的及优缺点, 本篇文章来做一个倒计时的简单动画

setInterval

setInterval 是 JS 中的一个定时器, 用于重复调用一个函数或执行一个代码段,在每次调用之间具有固定的时间延迟

下面实现一个定时器的例子

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
    <title>JS Bin</title>
    <style>
      #div{
      width: 100px;
      height: 100px;
      border: 1px solid red;
      text-align: center;
      line-height: 100px;
    }
    </style>
</head>
<body>
<div id="div">10</div>
<script>
  const div = document.getElementById('div')
  let text = parseInt(div.innerText)
  let timeId = setInterval(()=>{
   div.innerText = text--
   if(text < 0) clearInterval(timeId)
  },1000)
</script>
</body>
</html>

这是一个从 10 ~ 0 的倒计时简单动画, 可以发现用 setInterval 实现倒计时很简单, 但是这个 api 有几个缺点

  1. setInterval 某些间隔会被跳过
  2. 多个定时器的处理时间会不同

来画个图

假设我们在一个事件处理函数中注册了一个 200ms 间隔的 setInterval, 但是定时器最小执行时间为 10ms, 即使设置为 0, 也会改为 10ms, 详见 W3C

所以我们在 10ms 左右的时间里注册了一个定时器, 那么在事件处理函数中我们假设处理了 200~300ms, 那么在事件处理完才会执行定时器, 在执行第一个定时器时又会注册第二个定时器, 那么在 600 多毫秒的时候第三个定时器可能不会被加入到任务队列, 因为这时候第一个定时器任务还在执行, 第二个定时器任务加入到队列, 这就会产生跳过问题, 因此也就会导致动画的跳帧问题,

所以大多数情况下建议使用 setTimeout 来替代 setInterval

setTimeout

与 setInterval 不同的是 setTimeout 只执行一次, 所以他不会出现任务跳过和多个定时器时间不准的问题

利用递归来模拟, 只需稍微改一下代码

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
    <title>JS Bin</title>
    <style>
      #div{
      width: 100px;
      height: 100px;
      border: 1px solid red;
      text-align: center;
      line-height: 100px;
    }
    </style>
</head>
<body>
<div id="div">10</div>
<script>
  const div = document.getElementById('div')
  let text = parseInt(div.innerText)
  const run = ()=>{
    if(text < 0) return
    div.innerText = text--
    setTimeout(run,1000)
  }
  setTimeout(run,1000)
</script>
</body>
</html>

这样的连续回调保证了每次执行时间的线性相同, 也保证了每个计时器函数的依次执行, 比 setInterval 好很多

但是相对于动画来说它还不是最完美的解决方案, 因为 setTimeout 和 setInterval 都还是要执行时间间隔的, 这样就会与浏览器屏幕刷新时间不同步, 从而也会出现轻微的掉帧

requestAnimationFrame

这个 api 是 HTML5 的新特性, 用来请求动画, 名如其意, 就是相对帧来说去绘制动画

mdn 上解释说:

当你准备更新动画时你应该调用此方法。这将使浏览器在下一次重绘之前调用你传入给该方法的动画函数(即你的回调函数)。回调函数执行次数通常是每秒60次,但在大多数遵循W3C建议的浏览器中,回调函数执行次数通常与浏览器屏幕刷新次数相匹配。

通俗来说就是这个方法会随着浏览器的屏幕刷新屏幕来调整动画的帧数, 一帧也相当于一个图片, 很多帧合起来就变成了动画, 就和电影之类的媒介一样的原理

它最大的优势是由系统来决定回调函数的执行时机, 它能保证回调函数在屏幕每一次的刷新间隔中只被执行一次, 这样就不会引起丢帧现象, 也不会导致动画出现卡顿的问题

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
    <title>JS Bin</title>
    <style>
      #div{
      width: 100px;
      height: 100px;
      border: 1px solid red;
      text-align: center;
      line-height: 100px;
    }
    </style>
</head>
<body>
<div id="div">10</div>
<script>
  const div = document.getElementById('div')
  const step = (time) => {
  div.innerText = 10 - Math.floor(time / 1000)
    if (time < 10000) {
      window.requestAnimationFrame(step);    
    }
  }
  window.requestAnimationFrame(step);
</script>
</body>
</html>

time 参数就是这个回调开始执行的时间, 那我就规定当开始时间小于 10000ms, 那么就继续执行, 然后在用 10 减去这个执行时间, 那么就得到了一个倒计时

上面三种方法都可以实现倒计时动画, 但各自的优缺点不一, 如果想体验更加真实, 那你可以实现一个较为复杂的动画, 其中差别会比较明显

如果还没有用 requestAnimationFrame 做过动画的小伙伴, 赶快去试一下吧