深入理解requestAnimationFrame的动画循环

一、初识requestAnimationFrame

requestAnimationFrame解决了浏览器不知道javascript动画什么时候开始、不知道最佳循环间隔时间的问题。它是跟着浏览器的绘制走的,如果浏览器绘制间隔是16.7ms,它就按这个间隔绘制;如果浏览器绘制间隔是10ms, 它就按10ms绘制。这样就不会存在过度绘制的问题,动画不会丢帧。

内部是这么运作的:

浏览器页面每次要重绘,就会通知requestAnimationFrame;
这是资源非常高效的一种利用方式。

有以下两点:

1、就算很多个requestAnimationFrame()要执行,浏览器只要通知一次就可以了。而setTimeout是多个独立绘制。

2、一旦页面不在当前页面(比如:页面最小化了),页面是不会进行重绘的,自然requestAnimationFrame也不会触发(因为没有通知)。页面绘制全部停止,资源高效利用。

二. 动画的循环间隔

编写动画循环的关键,是要知道延迟时间多长合适。一方面,循环时间必须足够短,这样才能保证动画效果更平滑流畅;另一方面,循环还要足够长,这样才能保证浏览器有能力渲染产生的变化。大多数显示器的刷新频率是60Hz,相当于每秒钟重绘60次。大多数浏览器都会对重绘操作加以限制,不超过显示器的重绘频率,因为即使超过了这个频率,用户体验也不会有提升。

因此最平滑动画的最佳循环间隔是1000ms/60,约等于17ms。以这个循环间隔重绘的动画是平滑的,因为这个速度最接近浏览器的最高限速。为了适应17ms的循环间隔,多重动画可能需要加以节制,以便不会完成得太快。

虽然与使用多组setTimeout()相比,使用setInterval()的动画循环效率更高。但是无论setTimeout()还是setInterval()都不十分精确。为它们传入的第二个参数,实际上只是指定了把动画代码添加到浏览器UI线程队列以等待执行的时间。如果队列前面已经加入了其他任务,那动画代码就要等前面的任务执行完成后再执行。如果UI线程繁忙,比如忙于处理用户操作,那么即使把代码加入队列也不会立即执行。

因此,知道什么时候绘制下一帧是保证动画平滑的关键。然而,面对不十分精确的setTimeout()setInterval(),开发人员至今都没有办法确保浏览器按时绘制下一帧。

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

  IE8及其以下版本浏览器: 15.6ms;

  IE9及其以上版本浏览器:4ms;

  Firefox和Safari:10ms;

  Chrome:4ms。

更为复杂的是,浏览器开始限制后台标签页或不活动标签页的计数器。因此,即使你优化了循环间隔,可能仍然只能接近你想要的效果。

三. requestAnimationFrame()

Mozilla的 Robert O'Callahan 指出,CSS变换动画的优势在于浏览器知道动画什么时候开始,因此会计算出正确的循环间隔,在适当的时候刷新UI。而对于JavaScript动画,浏览器就无从知晓什么时候开始。

因此Robert O'Callahan的方案是,创建一个新方法mozRequestAnimationFrame() ,通过它告诉浏览器某些代码将要执行动画。这样浏览器可以在运行某些代码后进行适当的优化。

setTimeout()setInterval()方法不同,requestAnimationFrame()不需要调用者指定帧速率,浏览器会自行决定最佳的帧效率。

requestAnimationFrame()方法接收一个参数,即在重绘屏幕前调用以个函数。这个函数负责改变下一次重绘时的DOM样式。为了创建动画循环,可以像使用setTimeout()一样,把多个对requestAnimationFrame()的调用连缀起来。

如:

function drawFrame() {

    window.requestAnimationFrame(drawFrame);

    // animation code...

}

window.requestAnimationFrame(drawFrame);

四. requestAnimationFrame()的兼容性

4.1 requestAnimationFrame()的兼容性封装:

由于mozRequestAnimationFrame()是HTML5的新功能,目前各大浏览器的支持情况各异。如果希望代码具备更好的跨平台性,可以考虑使用下面的代码实现各平台兼容性:

if(!window.requestAnimationFrame) {

    window.requestAnimationFrame = (window.webkitRequestAnimationFrame ||

    window.mozRequestAnimationFrame ||

    window.oRequestAnimationFrame ||

    window.msRequestAnimationFrame ||

        function(callback) {

            let self =this, start, finish;

            return window.setTimeout(function() {

            start = + new Date();

            callback(start);

            finish = + new Date();

            self.timeout = 1000/60 - (finish - start);

        }, self.timeout);

    });

}

这段代码先检查了window.requestAnimationFrame函数的定义是否存在。如果不存在,就遍历已知的各种浏览器实现并替代该函数。如果还是找不到一个与浏览器相关的实现,它最终会采用基于JavaScript定时器的动画以每秒60帧的间隔调用setTimeout函数。

mozRequestAnimationFrame()会接收一个时间码(从1970年1月1日起至今的毫秒数),表示下一次重绘的实际发生时间。这样,mozRequestAnimationFrame()就会根据这个时间码设定将来的某个时刻进行重绘。

但是webkitRequestAnimationFrame()msRequestAnimationFrame()不会给回调函数传递时间码,因此无法知道下一次重绘将发生在什么时间。

如果要计算两次重绘的时间间隔,Firefox中可以使用既有的时间码,而在Chrome和IE则可以使用不太精确地Date()对象。

4.2 cancelRequestAnimFrame()的兼容性封装:

W3C也提供了cancelRequestAnimationFrame()方法,用于取消回调函数。requestAnimationFrame()方法会返回一个对象,用做标识回掉函数身份。若要取消回调函数的执行,可将其传给cancelRequestAnimationFrame()

window.cancelRequestAnimFrame = (function() {

    return window.cancelAnimationFrame ||

        window.webkitCancelRequestAnimationFrame ||

        window.mozCancelRequestAnimationFrame ||

        window.oCancelRequestAnimationFrame ||

        window.msCancelRequestAnimationFrame ||

        clearTimeout;

} )();

4.3 requestAnimationFrame()升级版封装方法:

另外还有一种更优雅的requestAnimationFrame()的兼容性封装方法:

(function() {

    let lastTime = 0;

    let vendors = ['ms', 'moz', 'webkit', 'o'];

    for(let x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {

        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];

        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame'] || window[vendors[x]+'CancelRequestAnimationFrame'];

    }

    if (!window.requestAnimationFrame)

    window.requestAnimationFrame = function(callback, element) {

        let currTime = new Date().getTime();

        let timeToCall = Math.max(0, 1000/60 - (currTime - lastTime));

        let id = window.setTimeout(function() { 

           callback(currTime + timeToCall); },

        timeToCall);

        lastTime = currTime + timeToCall;

        return id;

    };

    if(!window.cancelAnimationFrame)

    window.cancelAnimationFrame =function(id) {

        clearTimeout(id);

    };

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

推荐阅读更多精彩内容