requestIdleCallback-后台任务调度

1. 写在前面

在学JS的最初,我们就了解到一个情况,就是JS是单线程的,它只有执行完一段代码之后,才能执行另外的代码,在平时,这其实并不会受到影响,但是当你需要一些高频的操作时呢?比如你使用JS来完成一段动画,监听input的输入来频繁的操作DOMscroll的滚动监听等,这个时候,我们多么希望,把这些计算量特别大的功能,直接另开一个线程去处理。

2. Worker

说到多线程,可能你就想到了Worder,是的,Worker是一个多线程的功能,但Worker有个很大的限制,就是Worker只能进行一些单纯的JS计算,不能牵扯到DOM,而在JS很多动态效果中,有特别多的地方,是进行操控DOM元素的,而DOM元素的操作,才是最消耗性能的地方,这个时候Worker就显得有点鸡肋,所以虽然Worker方法已经出来了很多年了,但依然是不温不火。

本篇当然不是为了来说这个受限特别多的Worker,而是一个新的API,它的名字叫做:requestIdleCallback

3. requestIdleCallback

在很久以来,前端的开发者们,都希望可以通过一种方法,来了解到,当前的事件处理队列的情况,是否浏览器当前处于空闲状态,还是正在处理一个很复杂计算量很大的代码,比如:数据上报,数据分析,客户端模板渲染,数据的预加载等,这些操作,更适合于在浏览器空闲的时候进行处理,也就是一个概念中的“后台处理程序”。

如果当前正在处理很复杂的逻辑,而又要处理上述的逻辑的话,那么事件处理队列将变得异常的庞大,这个时候,浏览器甚至会出现假死的状态,而在以往的时候,当我们猜测某些操作,会出现这种情况的时候,通常使用setTimeout来做一个延时的处理,甚至把一个很复杂的逻辑,拆分为多个模块,然后分步的使用setTimeout来执行,虽然可以对这种情况有一定的缓解,但依然是不解决问题的,因为我们并不确定,每一个模块,具体会执行多长时间,而用户在这些模块计算的过程中,又会有其他的操作,这都是一种不确定性,导致了无法更有效的利用浏览器的性能。

前言说完了,那么就来说一下本篇文章中将要说到的一个新兴的API,它是2016年上半年,受限在chrome49中,被支持的,这也是开发者们,喜欢chrome的一个原因吧,总是会为前端开发者考虑,实现一些性能更好的API,帮助开发者们,完成一些难以处理的性能问题。

也是在2017年的年初,W3C也有了该API的草案:requestidlecallback,后台任务调度。

它是一个后台任务,只有在浏览器空闲的时候,才会被执行,也就是说,如果你现在正在执行一个requestAnimationFrame的动画,而这个动画消耗了特别多的浏览器性能,那么在该动画的间歇期,requestidlecallback才会被执行,而对于一些性能不好的浏览器,requestAnimationFrame动画,可能占据了特别多的内存,那么这个时候,requestidlecallback就可能是在整个动画执行完毕之后,才会被执行,这样至少对于动画来说,会有更好的体验,而不会因为中间加入了其他的操作,而导致了动画不流畅。

而动画结束之后,浏览器的事件队列中,没有其他的操作时,这个时候再来执行requestidlecallback的方法。甚至为了更精准的提供这些信息,requestidlecallback的回调函数中,会传入一个deadline对象,该对象中有方法,可以对事件执行的事件消耗进行一个预估。这样前端的开发者们,就可以通过deadline对象来预估这个回调函数,大概需要消耗的时间,进而做出正确的操作,保证不会影响到其他的动画等体验性更强的操作。

看起来还是很让人兴奋的嘛,那浏览器的兼容呢,我是不是现在就可以用于生产实践呢?去看看吧:requestIdleCallback兼容;

还有不少的发展空间嘛,不过想想,按照现在的发展速度已经浏览器,设备的更新速度,再过两年,移动端的应该是可以使用了吧~~

既然再有两年,就可能会被应用到了生产环境去了,那么现在是不是要开始学起来呢,接下来,就更细致的来看一下,requestidlecallback的用法吧。

3.1 默认简单方法使用

最简单的调用方式:


requestId = requestIdleCallback(cb);


这里的requestIdsetTimeoutsetInterval的返回值一样,是一个标识符,如果在之后,希望清理掉该回调的话,可以直接cancelIdleCallback(requestId)即可,关于这一点,与计时器是完全相同的。

前面也说过了一点,回调函数会传入一个默认的deadline对象,它是IdleDeadline构造函数的一个实例,该构造函数,只支持两个属性


didTimeout : Boolean
// 是否超时触发,(只读)

timeRemaining : function
// 该帧剩余可用时间


其中,只包含一个属性didTimeout(只读),和一个方法timeRemaining

deadline就是这样一个基于requestIdleCallbackIdleDeadline的实例,默认情况下,它也只包含这两个可用的数据。

去测试页面看看吧:第一次实战演练

在接下来,我们就来进一步更深入的了解一下这两个属性吧。

3.2 didTimeout

说到这个属性的话,就不得不提一下requestIdleCallback的第二个参数,它是一个可配置的对象,只支持一个参数,timeout,如果一帧内,一直没有空闲的时间可以执行requestIdleCallback的回调函数的话,那么当到达timeout设置的超时时间,requestIdleCallback就不在保持原有的效果了,而是在到达超时时间时,立即把回调推入到正在执行的事件列表中,这个时候,requestIdleCallback的表现就与setTimeout的表现一致了。

既然是说到了这里,那么我们就来看个例子吧:第二场实战演练(timeout,didTimeout)

在上述的示例中,主要是测试了以下两种情况


//cb这里不加了,细节在前面的示例链接中
function rIC(){
    requestId = requestIdleCallback(cb,{
        timeout : 100
    });
}

function _cb1(){
    _reset();

    var i = 0,
        len = 5;

    function _s(){

        if(i < len){
            add("i="+i);
            rAF(_s);
            i++;
        }
    }

    //requestIdleCallback的调用
    rIC();

    //rAF(_s);
    _s();
    //这里的两种调用方式,会对rIC有什么区别呢?
    //在本篇文章的最后,会进行一下说明。

}


function _cb2(){
    _reset();

    var i = 0,
        len = 10000;

    rIC();

    add("循环了10000次,1000的倍数才会显示!");
    for(i;i<len;i++){
        if(i%1000 == 0){
            add("i="+i);
        }

        //用来阻塞时间
        console.log("i="+i);
    }

}


那么timeout是必须要设置的吗?这就要看具体情况了,如果这个方法,必须要在某段时间内执行,那么可以设置timeout,如果没有这个必要,那么可以完全不用管这个参数的,直接简单的调用即可。

timeoutdidTimeout的相关内容,就到这里了。

3.3 timeRemaining

在回调函数传入的参数deadline对象中,唯一的一个方法是:timeRemaining(),它是用来获取当前一帧还有多长时间结束的。

如何理解这个呢,先来看一张图,该图是取自W3C官方文档中的:

该图中的frame#1frame#2就是两个帧,每个帧的持续时间是(100/60 = 16.66ms),而在每一帧内部,TASKredering只花费了一部分时间,并没有占据整个帧,那么这个时候,如图中idle period的部分就是空闲时间,而每一帧中的空闲时间,根据该帧中处理事情的多少,复杂度等,消耗不等,所以空闲时间也不等。

而对于每一个headline.timeRemaining()的返回值,就是如图中,Idle Callback到所在帧结尾的时间(ms级)。

对于该属性,在前面的示例中,就有用到了,所以这第三次实战,就继续使用前面的示例吧:第二次实战演练(timeRemaining)

前面的示例,考虑的情况并不多,如果帧中处理的东西特别多呢?比如使用rAF(requestAnimationFrame)处理,但是其每次处理,都消耗特别多,无法在一帧内执行呢?

所以,这里来看真正的第三次实战:第三次实战演练(timeRemaining)

在示例三种,是为了说明本小节前面的那张图,做的专门的处理,也验证了前者图中所示的东西。

但是,我们却忽略了一点,在所有前面的示例中,rIC(requestIdleCallback)的回调函数,处理的都是很简单的问题,那么如果回调函数,处理的是一个很复杂的问题呢?而这个很复杂的问题在时间上的消耗,甚至大于一个帧的时间呢?

不管如何说,都不是实战来说的明白,所以继续实战:第四次实战(rIC复杂回调)

在第四次实战中,rIC回调,都是计算量特别大的回调,但是得到的效果却截然不同,

不做任何处理,在一次回调中,处理所有的回调,结果直接阻塞掉了原有的rAF的处理,这样就会导致rAF的动画出现卡顿的情况,如果这样的话,那么rIC就变得有些鸡肋了。

看一下第四次实战示例中的截图:

从图中可以看出,rIC的一个回调处理,直接就消耗了305毫秒的时间,对于rAF的循环处理模块,简直是照成了致命的打击,如果rAF是动画的话,那么在这个时候,动画就会卡主305毫秒,用户是很明显可以感觉到这个停顿的,这对于用户体验来说,差到了极点,所以这样的情况是不能出现的。

那么如何避免呢,这就要靠本小节中的timeRemaining方法了,它可以获取每一帧的剩余空闲时间,而我们的这些处理,只需要在剩余的空闲时间来做就可以了,不需要实时性的处理,甚至,如果毎帧的空余时间短,我都接受多占用几个帧的空余时间,分段来处理掉这样问题。

所以在实例中的优化方法就是:把这个复杂的回调,分段处理,得到的结果就如下图所示:

由上图可以看到,在rIC消耗过多的时候,rAF依然可以正常的执行,rIC只有在rAF的空闲间隙中,在处理自己的事情,当rAF想要处获取执行权的时候,rIC立即交出这个权利,然后等待rAF忙完,再继续处理自己的问题。

简直完美么?是的,完美。

由于篇幅有限,本小节最重要的源代码,就不在本篇内部展示了,请到第四次实战中查看。

前面说的示例,就是timeRemaining的最重要的地方了,差不多就到这里吧。

3.3 cancelIdleCallback

既然可以设置一个requestIdleCallback的回调,那么也可以取消掉一个,它就是对应于requestIdleCallbackcancelIdleCallback方法。其调用方法特别简单,与setTimeout完全相同。

所以本篇没有这个示例,这里只做一个简单的代码展示


//定义一个
var requestId = requestIdleCallback(function cb1(deadline){
    console.log("deadline.timeRemaining="+deadline.timeRemaining());
});

//取消掉
cancelIdleCallback(requestId);


就是如此简单…

3.4 备注事项

1.timeRemaining有一个特性需要注意一下,那就是它获取的值,最大不会操过50ms,也就是说,就算没有使用rAF的循环模式,在代码中,没有任何这样的循环的话,timeRemaining的取值,也不会大于50ms的,这个可以自己做一个简单的示例。copy如下代码,直接到控制台执行即可:


requestIdleCallback(function cb1(deadline){
    console.log("deadline.timeRemaining="+deadline.timeRemaining());
});


为什么会这样呢,因为这个是W3C中的一个标准…来看下W3C中,该情况的一个描述图:

它在说明,如果没rAF这样的循环处理,浏览器一直处于空闲状态的话,deadline.timeRemaining可以得到的最长时间,也是50ms,去参考原文:requestidlecallback

2. requestIdleCallbakc的执行与requestAnimationFrame有一个相同的特性,不管当前帧,是否有空闲时间,它的最早执行时间,都是在下一帧开始的,如果您有注意的话,这个可以在前面的示例中看到效果,就在第二次实战演练中哦,注意左侧代码中,有一段这样的代码:


//rAF(_s);
_s();
//这里的两种调用方式,会对rIC有什么区别呢?
//在本篇文章的最后,会进行一下说明。


3. 浏览器对后来的动画做过一些优化,如果当前页面没有处于激活态的话,那么该页的空闲时间,rIC回调,就不会高频的去触发,而是会每隔10s才会触发一次,以节省设备的功耗,该方案在CSS3动画,(从跑马灯说起中,有这样的示例)requestAnimationFrame的动画中,都有这样的处理的。

W3C标准中,是这样解释的:

When the user agent determines that the web page is not user visible it can throttle idle periods to reduce the power usage of the device, for example, only triggering an idle period every 10 seconds rather than continuously.

OK,到这里本篇基本就结束了。

4. 应用

再好的东西,如果不能使用,那么就等于没用。

requestIdleCallback适合于哪里呢?可以想象一下,在我们的产品中,会收集特别多的信息,比如最常见的点击流。

对于用户来说,这个是完全不关心的,它基本上就是属于一个后台的活动,甚至如果支持多线程的话,它完全可以开一个额外的线程,专门做这个处理,而当内存吃紧时,暂停也不影响的。

这个时候的数据收集,就特别的适合使用requestIdleCallback

或者也可以认为,其他的所有,与用户的实时操作无关,JS在默默执行的操作,都可以使用requestidlecallback来处理,而且,这样还能更好的分配资源,在同样性能的设备上,可以达到更好的用户体验。

比如:

1:大型项目,JS文件过多过大,提升首屏速度,有些JS要延后执行,但是使用setTimeout并不一定能真的延后执行,setTimeout只是在某段时间之后,把回调函数,推入执行队列。这个时间并不是太好掌控。

2:input,scroll事件,setTimeout其实在牺牲一部分体验,而之所以使用setTimeout是因为我们无法判断浏览器当前是否在处理特别多的事情,所以不得不牺牲一部分体验,来达到防止浏览器假死的状态。

3:在input中,使用该方法,是最好的一种,如果没有执行,就直接cancelIdleCallback移除之前的绑定,然后继续下一次的requestIdleCallback的绑定,这样,在浏览器不繁忙的情况,不用担心体验的问题,而在浏览器繁忙的时候,就等于使用了一个setTimeout的方法。

甚至可以这么举个例子,一些频繁触发的事件,如果不是那种需要实时处理的,都可以尝试使用requestIdleCallback来做这个的哦。

非常期待,再过两年,这个方法可以被更多的浏览器支持。

总结

当设备的性能越来越好,浏览器支持的效果越来越炫,浏览器的开发者们,越来越多的开始考虑原生API来处理一些之前特别占用性能的功能了,自从最初的requestAnimationFrameInsterSectionObserver,还有requestIdleCallback,对于前端的将来,充满希望。

本篇到此为止。

如果您发现文中有描述错误或者不当的地方,请留言指出,不胜感激,谢谢!

本文属于原创文章,如需转载,请注明出处,谢谢!

本文地址:http://www.zhangyunling.com/?p=702

发表评论

电子邮件地址不会被公开。 必填项已用*标注

您可以使用这些HTML标签和属性: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>