用requestAnimationFrame封装一个定时器

908 阅读3分钟

本文已参与「新人创作礼」活动,一起开启掘金创作之路。 点击查看活动详情

写在前面

setInterval实现动画有很多缺点,首先是精度问题,如果有大量的dom操作,动画间隔是不准确的。

requestAnimationFrame会将所有操作集中起来,跟随浏览器的刷新频率(60帧)一次完成,会区分隐藏不可见的元素,性能更好。 但是requestAnimationFramesetInterval对时间的处理方式是不一样的,setInterval直接传递一个时间间隔(ms单位),以此执行回调函数

setInterval(() => {
    //
}, 1000)

requestAnimationFrame会传递一个时间戳timestamp,表示自页面加载以来,回调函数被执行的时刻,一直增大。

13.502 
30.098 
46.995 
63.45 ...

requestAnimationFrame时间间隔处理

通过时间戳可以得到时间间隔从而进行处理,也就是说每个动画还需要一个变量来记录时间来和timestamp进行对比。

let time = 0;
function A(t) {
    requestAnimationFrame(A)
    console.log("距离上一次执行的时间间隔", t - time)
    time = t
}
A()
// 距离上一次执行的时间间隔 16.64300000000003
// 距离上一次执行的时间间隔 16.663000000000466
// 距离上一次执行的时间间隔 16.87199999999939
// 距离上一次执行的时间间隔 16.472000000000662
// 距离上一次执行的时间间隔 16.639999999999418
// 距离上一次执行的时间间隔 16.649000000000342
// 距离上一次执行的时间间隔 16.842999999999847
// 距离上一次执行的时间间隔 16.509000000000015
// 距离上一次执行的时间间隔 16.61999999999989
// 距离上一次执行的时间间隔 16.646999999999935
// 距离上一次执行的时间间隔 16.789999999999964
// 距离上一次执行的时间间隔 16.605999999999767
// 距离上一次执行的时间间隔 17.220000000000255
// 距离上一次执行的时间间隔 16.038000000000466
// 距离上一次执行的时间间隔 16.787999999999556
// 距离上一次执行的时间间隔 16.576000000000022
// 距离上一次执行的时间间隔 16.699999999999818
// 距离上一次执行的时间间隔 16.634000000000015
// 距离上一次执行的时间间隔 16.72400000000016
// 距离上一次执行的时间间隔 16.775000000000546

可以看出,requestAnimationFrame需要自行控制执行的时间间隔,假如要实现一个秒计时器,通过计算执行时间差来判断是否执行

let interval = 1000
let lastTime = 0
function SecondTimer(t) {
    requestAnimationFrame(SecondTimer)
    if(t - lastTime >= interval) {
        console.log("1s到了")
        lastTime = t
    }
}
SecondTimer()

封装定时器

我们将timestamp的处理逻辑抽取出来,调用的时候只需要考虑时间间隔和回调,这样使用方式就和setInterval一样了

class Timer {
    constructor(fn, interval) {
        this.interval = interval
        this.fn = fn
        this.lastTime = 0

        this.loop(0)
    }
    loop(timestamp){
        this.timer = requestAnimationFrame(Timer.prototype.loop.bind(this))
        if(timestamp - this.lastTime > this.interval) { 
            this.lastTime = timestamp;
            typeof this.fn == "function" && this.fn()
        }
    }
    clear() {
        cancelAnimationFrame(this.timer)
        this.timer = null
    }
}
// 调用
let t = new Timer(() => {
    console.log("timer 1s到了")
}, 1000)
let t2 = setInterval(() => {
    console.log("setinterval 1s到了")
}, 1000)

image.png

中间我把浏览器页面切走了,过几秒再切回来,可以看到,timer是没有执行的,而setInterval是一直在执行的,由于他俩执行机制的不一样,所以最好不要混用。

实现元素动画

前面我们实现了一个简单的canvas库 100行代码写个canvas库 ,里面舞台的刷新就是用的requestAnimationFrame,默认以16.67ms的频率更新舞台

render() {
    requestAnimationFrame(Stage.prototype.render.bind(this));
    this.clear();
}

但是很明显,每个单独的元素动画的频率肯定是不一样的,比如小人500ms动一次,而小车100ms就要动一次了,所以在处理元素动画的时候,就可以用上封装的这个定时器了

class Person {
    // 
    move() {
        this.timer = new Timer(() => {
            this.run()
        }, 500)
    }
}
class Car {
    //
    move() {
        this.timer = new Timer(() => {
            this.run()
        }, 200)
    }
}

run5.gif