对setTimeout和requestAnimationFrame的理解

事件环

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

事件环.gif

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

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

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

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
    <style>
      #divContainer {
        width: 300px;
        height: 300px;
        background-color: red;
      }
      #divContainer2 {
        width: 300px;
        height: 300px;
        background-color: yellow;
      }
    </style>
  </head>
  <body>
    <div id="divContainer">timeoutDiv</div>
    <div id="divContainer2">animateDiv</div>
    <script>
      let marginGap = 0;
      let marginGap2 = 0;

      function animateTimeout() {
        divContainer.style.marginLeft = `${++marginGap}px`;
        setTimeout(animateTimeout, 0);
      }
      function animateReq(req) {
        divContainer2.style.marginLeft = `${++marginGap2}px`;
        requestAnimationFrame(animateReq);
      }

      animateTimeout();
      animateReq();
    </script>
  </body>
</html>

这里定义了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倍。

参考

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 157,298评论 4 360
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 66,701评论 1 290
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 107,078评论 0 237
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,687评论 0 202
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,018评论 3 286
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,410评论 1 211
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,729评论 2 310
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,412评论 0 194
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,124评论 1 239
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,379评论 2 242
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 31,903评论 1 257
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,268评论 2 251
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 32,894评论 3 233
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,014评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,770评论 0 192
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,435评论 2 269
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,312评论 2 260