requestAnimationFrame,这个方法应该都不陌生,字面含义是请求动画帧。从API命名来看,和动画有着密切的关系。其用法跟setTimeout差不多,与setTimeout相比,最大的优势是由浏览器来决定函数的执行时机。形象一点的解释就是:告诉浏览器说 “我这里有一个函数要执行,你有空了帮忙执行一下”,然后浏览器相对比较空闲的时候就给执行了。

用法一:动画

raf api本身的设计就是用来解决js动画的性能问题。那么,为什么raf做动画性能会更好呢?主要原因在于raf更加智能,它并非加快执行速度,而是适当时候降帧,防止并解决丢帧问题。当它发现无法维持60fps的频率时,它会把频率降低到30fps来保持帧数的稳定。也就是说如果上一次raf的回调执行时间过长,那么触发下一次raf回调的时间就会缩短,反之亦然,这也是为什么说由浏览器来决定执行时机性能会更好。

(function animloop(){
  window.requestAnimFrame(animloop);
  render();
})();

DEMO:

See the Pen bErOLz by hugo (@baofen14787) on CodePen.

用法二: 函数节流

在高频率事件中,为了防止16ms内发生多次函数执行,使用raf可保证16ms内只触发一次,这既能保证流畅性也能更好的节省函数执行的开销。16ms内函数执行多次没有意义,因为显示器16ms刷新一次,多次执行并不会在界面上有任何显示。

举个例子:

var $box = $('#J_num2'),
      $point = $box.find('i');
$box.on('mousemove',function(e){
  requestAnimationFrame(function(){
      $point.css({
          top : e.pageY,
          left : e.pageX
      })
  })

})

用法三:CPU节能

raf的另一个特性是:如果页面不是激活状态下的话,函数会自动暂停,有效节省了CPU开销。在移动端,如果页面中有自动播放的轮播图、倒计时或使用setTimeout/setInterval来执行任务的定时器。那么当app进到后台或是锁屏后,webviewcorethread仍然持续占用CPU,导致耗电。而使用raf可以很简单的解决此类问题。

PS:复杂情况下,可配合window.onblur & window.focusdocument.onvisibilitychangedocument.onpause & document.onresume等相关的方法进行实现,但这篇文章里不做深入探讨。

(function(){
        var timer;
        var $txt = $('#J_num2'),
                num = 0;
        function play(){
            timer = setTimeout(function(){
                  //使用raf实现非激活状态下不运行
                requestAnimationFrame(function(){
                    stop();
                    next();
                });
            },1000)
        }

        function stop(){
            clearTimeout(timer)
        }

        function next(){
            $txt.text(num++);
            play();
        }
        play();
    })();

DEMO:

See the Pen OMxodB by hugo (@baofen14787) on CodePen.

用法四:分帧初始化

都知道,raf的执行时间约为16.7ms,即为一帧。那么可以使用raf将页面初始化的函数进行打散到每一帧里,这样可以在初始化时降低CPU及内存开销

很多页面,初始化加载时,CPU都会有很明显的波动,就是因为大量的操作都集中到了一点上。

举个例子:

页面中有4个模块,A、B、C、D,在页面加载时进行实例化,一般的写法类似于:

$(function(){
    new A();
    new B();
    new C();
    new D();
})

而使用raf可将每个模块分别初始化,即每个模块都有16ms的初始化时间

    $(function(){
        var lazyLoadList = [A,B,C,D];
        $.each(lazyloadList, function(index, module){
        window.requestAnimationFrame(function(){new module()});
        }); 
        
    })

用法五:异步化

raf实际是一种异步化的操作,曾经setTimeout(function(){},0)一度成为解决了很多前端疑难杂症的法宝。而现在,可以用raf来代替。

6 thoughts on “requestAnimationFrame最佳实践

  1. 用法2中的实例代码是错的,这只是收集了一帧内的全部待执行函数,并归到下一帧重绘前一起执行,并没有阻止多次执行(节流)。要做到一帧内只执行一次,需要一个变量上锁,rAF内锁住,回调执行完解锁。

    Reply
    • raf 16ms执行一次,其实已经达到了节流的目的了。

      如果按你所说,收集全部待执行函数,那为什么如果去掉这个raf,在16ms内会执行至少2次呢?

      加上raf,代码执行一次;不加的话代码至少执行2+次。 这就是节流的意义啊。

      Reply
  2. 用法2和用法4的写法出错的地方是一样的。拿用法4举例:ABCD4个模块,假设每个模块执行10ms,不加raf,执行完共40ms,则这一帧的长度至少为40ms,产生了阻塞;直接用$.each循环里跑raf,相当于瞬间注册了4个raf事件,也就是当前帧不做ABCD的加载,第二帧内还是要同时做ABCD的执行,那还是要至少花费40ms,依然是阻塞,没做到“分帧”,仅仅就是把阻塞挪到下一帧而已。
    要实现分帧,即每帧仅执行一个任务,要写递归,在每个raf执行完毕之后再注册新的raf,才能保证每帧1个任务。
    看下这篇文章的第三节和第四节,附有测试,用递归写的raf才能保证每帧一个任务。一帧的时间到底能不能凑成最佳的16ms完全由任务执行时长决定,可以进一步做优化。

    Reply

Leave a reply

<a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong> 

required