# requestAnimationFrame

附原帖地址:https://juejin.im/post/5c3ca3d76fb9a049a979f429

在web应用中,实现动画效果的方法比较多,Javascript中可以通过定时器setTimeout来实现,css3可以使用transition和animation来实现,html5中的canvas也可以实现。除此之外,html5还提供了一个专门用于请求动态的Api,即requestAnimationFrame

# 1 / 什么是 requestAnimationFrame

  • HTML5新增的API,类似于setTimeout定时器
  • window对象的一个方法,window.requestAnimationFrame
  • 浏览器专门为动画提供的Api,让Dom动画,Canvas动画,SVG动画,WebGL动画等有一个统一的刷新机制(因此只能用于浏览器)

# 2 / requestAnimationFrame做了什么?

  • 浏览器重绘频率一般会在和显示器的刷新率保持同步。大多数浏览器采取W3C规范的建议,浏览器的渲染页面的标准帧率也为60FPS
  • 按帧对网页进行重绘 :该方法告诉浏览器希望执行动画并请求浏览器在下一次重绘之前调用回调函数来更新动画
  • 由系统来决定回调函数的执行时机 :在运行时浏览器会自动优化方法的调用
    • 显示器有固定的刷新频率(60/75hz),也就是说,每秒最多只能重绘60次或75次,requestAnimationFrame的基本思想让页面重绘的频率与这个刷新率保持同步
      • 比如显示器屏幕刷新频率为60Hz,使用requestAnimationFrame Api,那么回调函数就每 1000ms / 60 = 16.7ms 执行一次;如果显示器屏幕的刷新率为75Hz,那么回调函数就每 1000ms / 75 = 13.3ms 执行一次
    • 通过requestAnimationFrame调用回调函数引起的页面重绘或回流的时间间隔和显示器的刷新时间间隔相同,所以requestAnimation不需要像setTimeout那样传递时间间隔,而是由浏览器通过系统获取并使用显示器刷新频率

# 3 / requestAnimation用法

动画帧请求回调函数列表:每个Document都有一个动画帧请求回调函数列表,该列表可以看成是由<handle, callback>元组组成的集合

  • handle 是一个整数,唯一地标识了元组在列表中的位置, cancelAnimationFrame() 可以通过它来停止动画
  • callback 是一个无返回值的、形参为一个时间值的函数
  • 刚开始该列表为空

页面可见性

  • 当页面被最小化或者被切换为后台标签页时,页面为不可见,浏览器会触发一个 visibilitychange 事件,并设置 document.hidden 属性为 true
  • 当页面切换到显示状态,页面变为可见,同时触发一个 visibilitychange 事件,设置 document.hidden 属性为 false

  • 调用操作
    • setTimeout 相似,但是不需要设置间隔时间,使用一个回调函数作为参数,返回一个大于0的整数
    handle = requestAnimationFrame(callback)
    
    1
      - callback,是一个回调函数,在下次重绘动画时调用,该回调函数接收唯一参数,是一个高精度时间戳,指触发回调函数的当前时间(不用手动传入)
      - 返回值是一个long型的非零整数,是requestAnimationFrame回调函数列表中唯一的标识,表示定时器的编号,无其他意义
    
  • 取消操作
    cancelAnimation(handle)
    
    1
      - 参数是调用requestAnimationFrame时的返回值(唯一标识)
      - 取消操作没有返回值
    
  • 浏览器执行过程
    • 首先判断document.hidden属性是否为true(页面是否可见),页面处于可见状态才会执行后面的步骤
    • 浏览器清空上一轮的动画函数
    • requestAnimationFrame将回调函数追加到动画帧请求回调函数列表的末尾
      • 当执行requestAnimationFrame(callback)的时候,不会立即调用 callback 函数,只是将其放入队列,每个回调函数都有一个boolean标识cancelled,该标识初始值为false,并且对外不可见
      • 当浏览器再执行列表中的回调函数得时候,判断每个元组的callback的cancelled,如果为false,则执行callback。(当页面可见并且动画帧请求回调函数列表不为空,浏览器会定期将这些回调函数加入到浏览器UI线程的队列中)
      • 当调用cancelAnimationFrame(handle)时,浏览器会设置该handle指向的回调函数的cancelled为true(无论该回调函数是否在动画帧请求回调函数列表中)。如果该handle没有指向任何回调函数,则什么也不会发生
  • 递归调用 - 想要实现一个完整的动画,应该在回调函数中递归调用回调函数
let count = 0
let rafId = null
/**
 * 回调函数
 * @param time requestAnimationFrame 调用该函数时,自动传入的一个时间
*/
function requestAnimation(time){
    console.log(time)
    // 动画没有执行完,则递归渲染
    if(count < 50){
        count++
        rafId = requestAnimationFrame(requestAnimation)
    }
}

requestAnimationFrame(requestAnimation)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
  • 如果在执行回调函数或者Document的动画帧请求回调函数列表被清空之前多次调用requestAnimationFrame来调用同一个回调函数,那么列表中会有多个元组指向该回调函数(它们的handle不同,但callback都为该回调函数),'采集所有动画'任务会执行多次该回调函数。(类比定时器setTimeout)
        function counter() {
            let count = 0;
            function animate(time) {
                if (count < 50) {
                count++;
                console.log(count);
                requestAnimationFrame(animate);
                }
            }
            requestAnimationFrame(animate);
        }
        btn.addEventListener("click", counter, false);
    
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    • 多次点击按钮,会发现打印出来多个序列数组(如下,连续触发三次,打印三个有序列)
    // reslut(截取部分)
    1
    20
    10
    2
    21
    11
    
    1
    2
    3
    4
    5
    6
    7
    • 如果是作用于动画,动画会出现突变的情况

# 4 / 优势

requestAnimationFrame 采用系统时间间隔,保持最佳绘制效率。不会因为间隔时间过短,造成过度绘制,增加开销;也不会因为间隔时间过长,使动画卡顿

从实现的功能和使用方法上,requestAnimationFrame 与 setTimeout 都相似,所以说其优势是同 setTimeout 实现的动画相比

  • 提升性能,防止掉帧
  • 节约资源,节省电源
  • 函数节流
    • 一个刷新间隔内函数执行多次时没有意义的,因为显示器每16.7ms刷新一次,多次绘制并不会在屏幕上体现出来
    • 在高频事件(reszie, scroll等)中,使用requestAnimationFrame可以防止在一个刷新间隔内发送多次函数执行,这样保证了流畅性,也节省了函数执行的开销
    • 某些情况下可以直接使用requestAnimationFrame替代Throttle函数,都是限制回调函数执行的频率

# 5 / 应用

  • 简单的进度条动画
<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <meta name="viewport" content="width=device-width, initial-scale=1.0">
        <title>requestAnimation</title>
        <style>
            #loadingBar {
                height: 30px;
                background: lightblue;
            }
        </style>
    </head>
    <body>
        <div id='loadingBar'>
            </button>
            <script>
                function loadingBar(ele) {
                    console.log(ele)
                        // 使用闭包保存定时器的编号
                    let handle;
                    return () => {
                        // 每次触发将进度清空
                        ele.style.width = "0";
                        // 开始动画前清除上一次的动画定时器
                        // 否则会开启多个定时器
                        cancelAnimationFrame(handle);
                        // 回调函数
                        let _progress = () => {
                            let eleWidth = parseInt(ele.style.width);
                            if (eleWidth < 200) {
                                ele.style.width = `${eleWidth + 5}px`;
                                handle = requestAnimationFrame(_progress);
                            } else {
                                cancelAnimationFrame(handle);
                            }
                        };
                        handle = requestAnimationFrame(_progress);
                    }
                }
                var loadingELE = document.getElementById('loadingBar')
                loadingBar(loadingELE)()
            </script>
    </body>
</html>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45