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

栏目: JavaScript · 发布时间: 7年前

内容简介:重复定时器,JS有一个方法叫做setInterval专门为此而生,但是大家diss他的理由很多,比如跳帧,比如容易内存泄漏,是个没人爱的孩子。而且setTimeout完全可以通过自身迭代实现重复定时的效果,因此setIntervval更加无人问津,而且对他退避三舍,感觉用setInterval就很low。But!setInverval真的不如setTimeout吗?请大家跟着笔者一起来一步步探索吧!无论是setTimeout还是setInterval都逃不过执行延迟,跳帧的问题。为什么呢?原因是事件环中JS

重复定时器,JS有一个方法叫做setInterval专门为此而生,但是大家diss他的理由很多,比如跳帧,比如容易内存泄漏,是个没人爱的孩子。而且setTimeout完全可以通过自身迭代实现重复定时的效果,因此setIntervval更加无人问津,而且对他退避三舍,感觉用setInterval就很low。But!setInverval真的不如setTimeout吗?请大家跟着笔者一起来一步步探索吧!

大纲

  • 重复定时器存在的问题

  • 手写一个重复定时器

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

重复定时器的各类问题

无论是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的偏差。

修复setTimeout的局限性

说到想要修正时间偏差,大家会想到什么?没错!就是获取当前时间的操作,通过这个操作,我们就可以每次运行的时候修复间隔时间,让总时长不至于偏差太大。

/*
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中:

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

浏览器中:

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

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

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

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

首先是毫无压力的情况下大家的性能,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)//点击清除数组内容,也就是清除了内存
复制代码
深度解密setTimeout和setInterval——为setInterval正名!

实践是唯一获取真理的方式。通过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)
    }
}
复制代码
深度解密setTimeout和setInterval——为setInterval正名!

结果惊喜不惊喜,函数运行完之后,内部的内存会自动释放,无需重置,然而全局变量却一直存在。也就是说变量的提升(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。

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

如果setInterval中存在无法回收的内容,那么这一部分内存就永远无法释放,这样就导致内存泄漏。所以还是编程习惯的问题,内存泄漏?setInterval不背这个锅。

垃圾回收(garbage collection)机制

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

reference-counting 引用计数

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

mark sweap

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

因为是从global开始标记的,所以函数作用域内的变量,函数完成之后就会释放内存。

通过垃圾回收机制,我们也可以发现,global中定义的内容要谨慎,因为global相当于是主函数,浏览器不会随便清除这一部分的内容。所以要注意,变量提升问题。

总结

并没有找到石锤表明setInterval是造成内存泄漏的原因。内存泄漏的原因分明是编码习惯不好,setInterval不背这个锅。


以上所述就是小编给大家介绍的《深度解密setTimeout和setInterval——为setInterval正名!》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!

查看所有标签

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。

Web Data Mining

Web Data Mining

Bing Liu / Springer / 2006-12-28 / USD 59.95

Web mining aims to discover useful information and knowledge from the Web hyperlink structure, page contents, and usage data. Although Web mining uses many conventional data mining techniques, it is n......一起来看看 《Web Data Mining》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试