对setTimeout和requestAnimationFrame的理解

事件环

来源 Jake Archibold的事件轮询演讲视频

事件环.gif

在刚开学使用javascript制作逐帧动画的时候使用的是setTimeout和setInterval这两个api来绘制动画帧。由于setInterval添加的事件队列会由于任务执行时间过长而导致队列添加出现错误,所以一般我都是用setTimeout来调用。代码很简单:

function animateTimeout() {
        animateFunc();
        setTimeout(animateTimeout)
}

只需要采用这样的一种调用方式,就能让你定义的animateFunc动作方法能够实现缓动播放的效果。起初我并未发现这样有何不妥,直到我使用了requestAnimationFrame这个api来实现了同样的缓动动画之后我就发现了差别。先看一下下面这段代码:



  
    
    
    Document
    
  
  
    
timeoutDiv
animateDiv

这里定义了divContainerdivContainer2两个盒子,分别使用setTimeoutrequestAnimationFrame来定时改变他们的左边距,达到一个平移的动画效果。然而我却发现使用setTimeout的移动速度明显比requestAnimationFrame快很多

animate.gif

可以在animateTimeout()animateReq()打印日志看看这两个方法的执行频率:

function animateTimeout() {
        console.log('logTimeout');
        divContainer.style.marginLeft = `${++marginGap}px`;
        setTimeout(animateTimeout, 0);
      }
      function animateReq(req) {
        console.log('logRequest');
        divContainer2.style.marginLeft = `${++marginGap2}px`;
        requestAnimationFrame(animateReq);
      }
image.png

可以看到animateReq()执行一次,animateTimeout()差不多会执行3次。所以会看到animateTimeout()的速度比animateReq()快了差不多3倍。那为什么会这样呢?于是我先查询了一下requestAnimationFrame的文档,发现这样一段说明文字

This will request that your animation function be called before the browser performs the next repaint. The number of callbacks is usually 60 times per second, but will generally match the display refresh rate in most web browsers as per W3C recommendation

依据文档所说,浏览器会在下次重绘之前调用requestAnimationFrame里面的回调方法,并且回调执行的频率是每秒60次,但是通常会按照W3C推荐的标准适配显示器的刷新频率。也就是说按照大多数显示器每秒60次的刷新频率来确定动画帧的时间是最合适的,这个时间段做出来的动画看起来是最平滑的。所以上面的例子中的setTimeout在调用的时候,延迟时间的参数0ms,那么一秒钟就会添加1000个动画函数的任务,页面会渲染1000次。但是这显然超过了显示器的每秒60次刷新频率,所以就会将这1000次的渲染任务适配到60次,那么每次渲染就会执行多次任务。要想使用setTimeout达到和显示器刷新频率相同的渲染那么在设置任务时间间隔的时候就要使用1000 / 60,大概是16.7ms渲染一帧的动画

function animateTimeout() {
        divContainer.style.marginLeft = `${++marginGap}px`;
        setTimeout(animateTimeout, 1000 / 60);
      }
timeoutAnimate.gif

可以看到这下animateTimeout()animateReq()的移动速度就差不多了。但是移动一段时间之后animateTimeout()还是跑到了animateReq()前面。使用setTimeoutsetInterval并不是那么精确。因为它们设置的时间间隔是将动画回调在指定的时间后添加到任务队列中,并不代表它会到时间就执行。此时如果主线程里面如果还有其他任务那么就不会去执行它,如果发生这种情况就会阻塞页面渲染,因为渲染是要等到脚本任务执行完成之后才会进行。而requestAnimateFrame是发生在渲染的时候,它能明确的知道动画什么时候开始,什么时候执行。

animate.gif

为什么会是将近3倍?

刚开始理解这一块的时候一直有疑问,为什么之前setTimeout延迟参数为0的时候只快3倍。因为按照每秒60次刷新的频率均摊下来的话,每次也应该要多执行1000 / 60大概16.7次才对。后来查阅一些资料才知道:setTimeout的延迟参数有一个默认的最小值4.7。当不传延迟参数,或者小于4.7的时候,setTimeout就是使用默认的4.7。所以真正多渲染的次数应该是16.7 / 4.7,将近3~4倍。

参考

  • https://www.youtube.com/watch?v=cCOL7MC4Pl0&t=890s

你可能感兴趣的:(对setTimeout和requestAnimationFrame的理解)