js中setTimeout,requestAnimationFrame,requestIdleCallback的区别和应用

版权声明:欢迎转载,转载请注明原始出处 https://blog.csdn.net/xiaomingelv/article/details/89057456

前言:

本文中提及的关于标题中三个api的回调函数的调用,都是指回调函数被压入调用队列中等待被调用,如对js中的异步调用和任务调度机制尚不太理解,建议先阅读作者的另一篇文章,异步调度机制——Javascript事件循环(Event Loop)机制解析

正文:

本文的主要内容是理清setTimeout,requestAnimationFrame,requestIdleCallback这三个函数的作用和调度机制,以及这三者的相关应用。

setTimeout

setTimeout这个函数对于大部分人来讲,应该算是比较熟悉的一个函数,请看以下代码

setTimeout(()=>{
            console.log('hello world')
        },100)

新手一般会理解为100秒后执行一个输出,但是,事实上,这个函数应该理解为在100秒后将回调函数添加到函数调用队列中,当浏览器执行完所有之前的操作后,会开始执行回调,实际上调用的延迟大体上是会大于100毫秒的,如需了解更多,可以戳上面的外部链接。这个函数事实上并没有什么好说的,只是为了用来跟下面两个函数作对比

requestAnimationFrame

首先,我们来了解一下一个概念:屏幕刷新频率,屏幕刷新率是指显示器的屏幕每秒钟会刷新多少次。目前,大部分的显示器的刷新频率为60次/秒,也就是说,屏幕刷新的间隔时间为大约1000/60=16.67ms。所以,一般来说,如果动画的执行频率跟屏幕的刷新频率一致,动画看起来就可以达到流畅的极限。

requestAnimationFrame和setTimeout有点相似,不同之处在于,setTimeout是在等待指定毫秒数之后被调用,而requestAnimationFrame则是在每次屏幕被刷新时被调用,注意,这里的屏幕刷新并不是指页面被刷新。requestAnimationFrame方法仅有一个参数,传入的这个方法将会在下一次屏幕刷新时被调用(实际上是被推入调用队列,并未立即执行),它的用法大体如下

window.requestAnimationFrame(function(){
            console.log('测试');
        })

上述的写法会在下一次屏幕刷新时输出一个字符串。

requestAnimationFrame相关应用

requestAnimationFrame的随系统的刷新频率被调用的特性使其非常适合用来做动画。对于很多小伙伴来说,如果用js来制作动画,可能会首先考虑setTimeout或者setInterval,间隔一段时间之后修改dom的样式,从而做出动画效果,但事实上用setTimeout做出来的动画性能比较差,会有卡顿,我们来看下列的例子。

假设屏幕每隔16.7ms刷新一次,我们使用setTimeout每隔10ms设置图像向左移动1px, 在页面当前无其他耗时任务事,大体上会出现如下绘制过程:

  • 第0ms: 屏幕未刷新,等待中,setTimeout也未执行,等待中;

  • 第10ms: 屏幕未刷新,等待中,setTimeout开始执行并设置图像属性left=1px;

  • 第16.7ms: 屏幕开始刷新,屏幕上的图像向左移动了1px, setTimeout 未执行,继续等待中;

  • 第20ms: 屏幕未刷新,等待中,setTimeout开始执行并设置left=2px;

  • 第30ms: 屏幕未刷新,等待中,setTimeout开始执行并设置left=3px;

  • 第33.4ms:屏幕开始刷新,屏幕上的图像向左移动了3px, setTimeout未执行,继续等待中;

这个时候我们就会发现,动画并不流畅了,图像突然间往左一次性走了2px

而利用requestAnimationFrame我们可以像下面这样使用

var degrees = 0;
function update() {
  img.style.transform = "translateX(" + degrees + "px)";
  degrees = degrees - 1;
  window.requestAnimationFrame(update);
}
window.requestAnimationFrame(update);

这样,在每次屏幕刷新的时候,图像都会往左走一个px,达到最大的流畅程度。当然,requestAnimationFrame并不仅仅只用于动画的绘制,事实上可以用它来优化页面的性能,具体读者可以根据需求自行摸索

requestIdleCallback

requestIdleCallback跟setTimeout和requestAnimationFrame又有所不同。试想一下各位有没有过这样的需求,因为JavaScript的单线程特性,为了提升性能,我们有时候会想把一些不是特别重要的或者一些非常耗时的操作放在在浏览器空闲的时候去执行,那怎么判断当前浏览器是否空闲呢,requestIdleCallback提供了这样的一个功能。

上面我们说到,requestAnimationFrame会在每次屏幕刷新的时候被调用,而requestIdleCallback则会在每次屏幕刷新时,判断当前帧是否还有多余的时间,如果有,则会调用requestAnimationFrame的回调函数,我们来看一下下面这张图片,图片中是两个连续的执行帧,大致可以理解为两个帧的持续时间大概为16.67,图中黄色部分就是空闲时间。所以,requestIdleCallback中的回调函数仅会在每次屏幕刷新并且有空闲时间时才会被调用

requestIdleCallback接受两个参数,requestIdleCallback(callback[, options]),第一个参数是一个回调函数,options是一个对象,里面仅支持一个属性:timeout,用于指定被调用的最后期限。

当回调参数被调用时,callback会被传入一个参数,结构如下

{didTimeout:false:false,timeRemaining:function{...}}

didTimeout用于表示是否是因为超时而被触发,timeRemaining是一个函数,它的执行结果表示当前帧剩余的空闲时间

接下来我们来看一下一个实际的例子

window.onload = function () {
    var count = 10;
    var i = 0;

    //往body中插入div
    function appendChildren(text) {
        var div = document.createElement("div");
        div.innerText = text;
        document.body.appendChild(div);
    }

    //测试requestAnimationFrame
    function testRequestAnimationFrameBusy() {
        var time = new Date();
        console.log(time.getTime());
        if (i < count) {
            i++;
            appendChildren(i);
            window.requestAnimationFrame(testRequestAnimationFrameBusy);
        }
    }
    
    //测试requestIdleCallback
    function testRequestIdleCallback() {
        requestIdleCallback(function (obj) {
            console.log(obj);
            appendChildren('requestIdleCallback发生');
        })

    }

    testRequestIdleCallback();
    testRequestAnimationFrameBusy();
};

这段代码中我们做了两件事,第一件事,我们通过requestAnimationFrame在每一次屏幕刷新时往body中添加一个div,然后当帧有空闲时间时,我们插入一个特殊的div表示requestIdleCallback被执行,上述代码我们可以看到页面的显示如下所示

如我们所料requestIdleCallback的回调函数在浏览器空闲的时候被执行了,而当我们把代码稍微修改一下,我们就会发现有些不同

window.onload = function () {
    var count = 10;
    var i = 0;

    //往body中插入div
    function appendChildren(text) {
        var div = document.createElement("div");
        div.innerText = text;
        document.body.appendChild(div);
    }

    //测试requestAnimationFrame
    function testRequestAnimationFrameBusy() {
         var time = new Date();
        console.log(time.getTime());
        for (var i = 0; i < 10; i++) {
            appendChildren(i);
        }
        time = new Date();
        console.log(time.getTime());
    }
    
    //测试requestIdleCallback
    function testRequestIdleCallback() {
        requestIdleCallback(function (obj) {
            console.log(obj);
            appendChildren('requestIdleCallback发生');
        })

    }

    testRequestIdleCallback();
    testRequestAnimationFrameBusy();
};

我们在testRequestAnimationFrameBusy中直接插入十个div,刻意造成浏览器完全无空闲的状态,我们就可以发现,requestIdleCallback的输出被排在了最后

当然,上面的代码我们都没有传入第二个参数,试想一下以下情况,如果浏览器一直很忙,那么就会导致一种情况,那就是回调一直不会被触发,那么这个时候,我们如何确保我们的回调一定会被执行呢,答案是传入第二个参数,并且指定timeout,像下面这样

requestIdleCallback(function (obj) {
            console.log(obj);
            appendChildren('requestIdleCallback发生');
        },{
            timeout:1000
})

这样做的话,如果浏览器一直处于忙碌状态,这是requestIdleCallback的表现和setTimeout是一致的,会在1000毫秒之后,把回调函数推入到事件队列中等待执行,而这种情况下,回调函数所接收到的对象,我们称之为deadline吧,deadline.didTimeout的值就会是true,表示当前是由于超时被触发的。

requestIdleCallback的相关应用

利用这个特性,我们可以在动画执行的期间,利用每帧的空闲时间来进行数据发送的操作,或者一些优先级比较低的操作,此时不会使影响到动画的性能,或者和requestAnimationFrame搭配,可以实现一些页面性能方面的的优化,可以参考下面这个链接,有比较详细的解析

【译】使用requestIdleCallback

参考资料:

阮一峰大神的博客

requestIdleCallback

window.requestAnimationFrame

猜你喜欢

转载自blog.csdn.net/xiaomingelv/article/details/89057456