深度解密setTimeout和setInterval——为setInterval正名!

前言

重复定时器,JS有一个方法叫作setInterval专门为此而生,可是你们diss他的理由不少,好比跳帧,好比容易内存泄漏,是个没人爱的孩子。并且setTimeout彻底能够经过自身迭代实现重复定时的效果,所以setIntervval更加无人问津,并且对他远而避之,感受用setInterval就很low。But!setInverval真的不如setTimeout吗?请你们跟着笔者一块儿来一步步探索吧!web

大纲

  • 重复定时器存在的问题ajax

  • 手写一个重复定时器算法

    • setTimeout的问题与优化
    • setInterval的问题与优化
  • 那些年setInterval背的锅——容易形成内存泄漏chrome

重复定时器的各种问题

不管是setTimeout仍是setInterval都逃不过执行延迟,跳帧的问题。为何呢?缘由是事件环中JS Stack过于繁忙的缘由,当排队轮到定时器的callback执行的时候,早已超时。还有一个缘由是定时器自己的callback操做过于繁重,甚至有async的操做,以致于没法预估运行时间,从而设定时间。编程

setTimeout篇

setTimeout那些事

对于setTimeout经过自身迭代实现重复定时的效果这一方法的使用,笔者最先是经过自红宝书了解的。数组

setTimeout(function(){ 
 var div = document.getElementById("myDiv"); 
 left = parseInt(div.style.left) + 5; 
 div.style.left = left + "px"; 
 if (left < 200){ 
    setTimeout(arguments.callee, 50); 
 } 
}, 50); 
复制代码

选自《JavaScript高级程序设计(第3版)》第611页浏览器

这应该是很是经典的一种写法了,可是setTimeout自己运行就须要额外的时间运行结束以后再激活下一次的运行。这样会致使一个问题就是时间不断延迟,本来是1000ms的间隔,再setTimeout无心识的延迟下也许会慢慢地跑到总时长2000ms的误差。bash

修复setTimeout的局限性

说到想要修正时间误差,你们会想到什么?没错!就是获取当前时间的操做,经过这个操做,咱们就能够每次运行的时候修复间隔时间,让总时长不至于误差太大。websocket

/*
id:定时器id,自定义
aminTime:执行间隔时间
callback:定时执行的函数,返回callback(id,runtime),id是定时器的时间,runtime是当前运行的时间
maxTime:定时器重复执行的最大时长
afterTimeUp:定时器超时以后的回调函数,返回afterTimeUp(id,usedTime,countTimes),id是定时器的时间,usedTime是定时器执行的总时间,countTimes是当前定时器运行的回调次数
*/
function runTimer(id,aminTime,callback,maxTime,afterTimeUp){
    //....
    let startTime=0//记录开始时间
    function getTime(){//获取当前时间
        return new Date().getTime();
    }
    /*
    diffTime:须要扣除的时间
    */
    function timeout(diffTime){//主要函数,定时器本体
        //....
        let runtime=aminTime-diffTime//计算下一次的执行间隔
        //....
        timer=setTimeout(()=>{
            //....
            //计算需扣除的时间,并执行下一次的调用
            let tmp=startTime
            callback(id,runtime,countTimes);
            startTime=getTime()
            diffTime=(startTime-tmp)-aminTime
            timeout(diffTime)
        },runtime)
    }
    //...
}
复制代码

启动与结束一个重复定时器

重复定时器的启动很简单,可是中止并无这么简单。咱们能够经过新建一个setTimeout结束当前的重复定时器,好比值执行20秒钟,超过20秒就结束。这个处理方案没有问题,只不过又多给了应用加了一个定时器,多一个定时器就多一个不肯定因素。网络

所以,咱们能够经过在每次执行setTimeout的是判断是否超时,若是超时则返回,并不执行下一次的回调。同理,若是想要经过执行次数来控制也能够经过这个方式。

function runTimer(id,aminTime,callback,maxTime,afterTimeUp){
    //...
    function timeout(diffTime){//主要函数,定时器本体
        //....
        if(getTime()-usedTime>=maxTime){ //超时清除定时器
            cleartimer()
            return
        }
        timer=setTimeout(()=>{
            //
            if(getTime()-usedTime>=maxTime){ //由于不知道那个时间段会超时,因此都加上判断
                cleartimer()
                return
            }
            //..
        },runtime)
    }
    function cleartimer(){//清除定时器
        //...
    }
    function starttimer(){
        //...
        timeout(0)//由于刚开始执行的时候没有时间差,因此是0
    }
    return {cleartimer,starttimer}//返回这两个方法,方便调用
}
复制代码

按照次数中止,咱们能够在每次的callback中判断。

let timer;
timer=runTimer("a",100,function(id,runtime,counts){
    if(counts===2){//若是已经执行两次了,则中止继续执行
       timer.cleartimer()
    }
},1000,function(id,usedTime,counts){})
timer.starttimer()
复制代码

经过上方按照次数中止定时器的思路,那么咱们能够作一个手动中止的方式。建立一个参数,用于监控是否须要中止,若是为true,则中止定时器。

let timer;
let stop=false
setTimeout(()=>{
    stop=true
},200)
timer=runTimer("a",100,function(id,runtime,counts){
    if(stop){
       timer.cleartimer()
    }
},1000,function(id,usedTime,counts){})
timer.starttimer()
复制代码

setInterval篇

setInterval那些事

你们必定认为setTimeout高效于setInterval,不过事实啪啪啪打脸,事实胜于雄辩,setInterval反而略胜一筹。不过要将setInterval打形成高性能的重复计时器,由于他之因此这么多毛病是没有用对。通过笔者改造后的Interval能够说和setTimeout不相上下。

将setInterval封装成和上述setTimeout同样的函数,包括用法,区别在于setInterval不须要重复调用自身。只须要在回调函数中控制时间便可。

timer=setInterval(()=>{
    if(getTime()-usedTime>=maxTime){ 
        cleartimer()
        return
    }
    countTimes++
    callback(id,getTime()-startTime,countTimes);
    startTime=getTime();
},aminTime)
复制代码

为了证实Interval的性能,如下是一波他们两的pk。

Nodejs中:

浏览器中:

在渲染或者计算没有什么压力的状况下,定时器的效率

在再渲染或者计算压力很大的状况下,定时器的效率

首先是毫无压力的状况下你们的性能,Interval完胜!

接下来是颇有压力的状况下?。哈哈苍天饶过谁,在相同时间,相同压力的状况下,都出现了跳帧超时,不过两人的缘由不同setTimeout压根没有执行,而setInterval是由于抛弃了相同队列下相同定时器的其余callback也就是只保留了了队列中的第一个挤进来的callback,能够说两人表现旗鼓至关。

也就是说在同步的操做的状况下,这二者的性能并没有多大区别,用哪一个均可以。可是在异步的状况下,好比ajax轮循(websocket不在讨论范围内),咱们只有一种选择就是setTimeout,缘由只有一个——天晓得此次ajax要浪多久才肯回来,这种状况下只有setTimeout才能胜任。

竟然setTimeout不比setInterval优秀,除了使用场景比setInterval广,从性能上来看,二者不分伯仲。那么为何呢?在下一小节会从事件环,内存泄漏以及垃圾回收这几个方面诊断一下缘由。

事件环(eventloop)

为了弄清楚为何二者都没法精准地执行回调函数,咱们要从事件环的特性开始入手。

JS是单线程的

在进入正题以前,咱们先讨论下JS的特性。他和其余的编程语言区别在哪里?虽然笔者没有深刻接触过其余语言,可是有一点能够确定,JS是服务于浏览器的,浏览器能够直接读懂js。

对于JS还有一个高频词就是,单线程。那么什么是单线程呢?从字面上理解就是一次只能作一件事。好比,学习的时候没法作其余事情,只能专心看书,这就是单线程。再好比,有些妈妈很厉害,能够一边织毛衣一边看电视,这就是多线程,能够同一时间作两件事。

JS是非阻塞的

JS不只是单线程,仍是非阻塞的语言,也就是说JS并不会等待某一个异步加载完成,好比接口读取,网络资源加载如图片视频。直接掠过异步,执行下方代码。那么异步的函数岂不是永远没法执行了吗?

eventloop

所以,JS该如何处理异步的回调方法?因而eventloop出现了,经过一个无限的循环,寻找符合条件的函数,执行之。可是JS很忙的,若是一直不断的有task任务,那么JS永远没法进入下一个循环。JS说我好累,我不干活了,罢工了。

stack和queue

因而出现了stack和queue,stack是JS工做的堆,一直不断地完成工做,而后将task推出stack中。而后queue(队列)就是下一轮须要执行的task们,全部未执行而将执行的task都将推入这个队列之中。等待当前stack清空执行完毕,而后eventloop循环至queue,再将queue中的task一个个推到stack中。

正由于eventloop循环的时间按照stack的状况而定。就像公交车同样,一站一站之间的时间虽然能够预估,可是不免有意外发生,好比堵车,好比乘客太多致使上车时间过长,好比不当心每一个路口都吃到了红灯等等意外状况,都会致使公交陈晚点。eventloop的stack就是一个不定因素,也许stack内的task都完成后远远超过了queue中的task推入的时间,致使每次的执行时间都有误差。

诊断setTimeout和setInterval

那些年setInterval背的锅——容易形成内存泄漏(memory leak)

说到内存泄漏就不得不说起垃圾回收(garbage collection),这两个概念绑在一块儿解释比较好,但是说是一对好基友。什么是内存泄露?听上去特别牛逼的概念,其实就是咱们建立的变量或者定义的对象,没有用了以后没有被系统回收,致使系统没有新的内存分配给以后须要建立的变量。简单的说就是借了没还,债台高筑。因此垃圾回收的算法就是来帮助回收这些内存的,不过有些内容应用不须要,然而开发者并无释放他们,也就是我不须要了可是死活不放手,垃圾回收也没办法只能略过他们去收集已经被抛弃的垃圾。那么咱们要怎样才能告诉垃圾回收算法,这些东西我不要了,你拿走吧?怎么样的辣鸡才能被回收给新辣鸡腾出空间呢?说到底这就是一个编程习惯的问题。

致使memory leak的最终缘由只有一个,就是没有即便释放不须要的内存——也就是没有释放定义的参数,致使垃圾回收没法回收内存,致使内存泄露。

那么内存是怎么分配的呢?

好比咱们定义了一个常量var a="apple",那么内存中就会分配出空间村粗apple这个字符串。你们也许会以为不就是字符串嘛,能占多少内存。没错,字符串占不了多少内存,可是若是是一个成千上万的数组呢?那内存占的可就不少了,若是不及时释放,后续工做会很艰难。

可是内存的概念太过于抽象,该怎么才能feel到这个占了多少内存或者说内存被释放了呢?打开chrome的Memory神器,带你体验如何感受内存。

这里咱们建立一个demo用来测试内存是如何工做的:

let array=[]//建立数组
createArray()//push内容,增长内存

function createArray(){
    for(let j=0;j<100000;j++){
        array.push(j*3*5)
    }
}
function clearArray(){
    array=[]
}

let grow=document.getElementById("grow")
grow.addEventListener("click",clearArray)//点击清除数组内容,也就是清除了内存
复制代码

实践是惟一获取真理的方式。经过chrome的测试工具,咱们能够发现清除分配给变量的内容,能够释放内存,这也是为何有许多代码结束以后会xxx=null,也就是为了释放内存的缘由。

既然咱们知道了内存是如何释放的,那么什么状况,即便咱们清空了变量也没法释放的内存的状况呢?

作了一组实验,array分别为函数内定义的变量,以及全局变量

let array=[]
createArray()
function createArray(){
    for(let j=0;j<100000;j++){
        array.push(j*3*5)
    }
}
复制代码
createArray()
function createArray(){
    let array=[]
    for(let j=0;j<100000;j++){
        array.push(j*3*5)
    }
}
复制代码

结果惊喜不惊喜,函数运行完以后,内部的内存会自动释放,无需重置,然而全局变量却一直存在。也就是说变量的提高(hoist)并且不及时清除引用的状况下会致使内存没法释放。

还有一种状况与dom有关——建立以及删除dom。有一组很经典的状况就是游离状的dom没法被回收。如下的代码,root已经被删除了,那么root中的子元素是否能够被回收?

let root=document.getElementById("root")
for(let i=0;i<2000;i++){
    let div=document.createElement("div")
    root.appendChild(div)
}
document.body.removeChild(root)
复制代码

答案是no,由于root的引用还存在着,虽然在dom中被删除了,可是引用还在,这个时候root的子元素就会以游离状态的dom存在,并且没法被回收。解决方案就是root=null,清空引用,消除有力状态的dom。

若是setInterval中存在没法回收的内容,那么这一部份内存就永远没法释放,这样就致使内存泄漏。因此仍是编程习惯的问题,内存泄漏?setInterval不背这个锅。

垃圾回收(garbage collection)机制

讨论完那些缘由会形成内存泄漏,垃圾回收机制。主要分为两种:reference-counting和mark sweap。

reference-counting 引用计数

这个比较容易理解,就是当前对象是否被引用,若是被引用标记。最后没有被标记的则清除。这样有个问题就是程序中两个不须要的参数互相引用,这样两个都会被标记,而后都没法被删除,也就是锁死了。为了解决这个问题,因此出现了标记清除法(mark sweap)。

mark sweap

标记清除法(mark sweap),这个方法是从这个程序的global开始,被global引用到的参数则标记。最后清除全部没有被标记的对象,这样能够解决两对象互相引用,没法释放的问题。

由于是从global开始标记的,因此函数做用域内的变量,函数完成以后就会释放内存。

经过垃圾回收机制,咱们也能够发现,global中定义的内容要谨慎,由于global至关因而主函数,浏览器不会随便清除这一部分的内容。因此要注意,变量提高问题。

总结

并无找到石锤代表setInterval是形成内存泄漏的缘由。内存泄漏的缘由分明是编码习惯很差,setInterval不背这个锅。

相关文章
相关标签/搜索