setTimeout浅析

刚学习javascript的时候,感觉setTimeout很好理解,不就是过n(传入的毫秒数)毫秒,执行以下传入的函数吗?这个理解伴随了我挺长的一段时间,才对setTimeout有了新的认识,请先看下面的例子:

 

var start = new Date()

setTimeout(function(){

    var end = new Date()

    console.log("时间间隔:", end - start, "ms")

}, 500)

while( new Date() - start < 1000 ){}

 

如果是刚开始学习javascript的我可能会得到500ms的结果,真实的结果大约是一个1010ms的结果,为什么会这样那?

 

要能清楚这个问题,要先知道浏览器是怎样运行javascript的。在大多数浏览器中,执行的javascript代码和用户的UI界面更新是共用一个线程的,它的工作原理是这样的:与线程对应着一个简单的队列,每当执行的javascript代码或更新用户的UI界面时,处理任务会先进入等待队列,当线程空闲时,最先进入队列的任务就会被提取出来运行。如果我现在点击一个网页中的 button按钮,并且点击按钮触发一个click事件。代码如下:

 

document.getElementById("btn").onclick = function(){
    dosomething()
    var div = document.createElement("div")
    div.innerHTML = "test"
    document.body.appendChild(div)
}

 

 

 

浏览器对刚才操作的处理大致如下图:

 

 

当点击button按钮时,浏览器会创建两个任务加入到线程的队列中。第一个任务是更新按钮的样式,让用户知道按钮被点击了;第二个任务是执行click事件触发对应的javascript代码。假设这个时候线程是空闲状态,那么第一个任务就会被提取出来并执行,然后第二个任务就会被提取出来运行,在执行过程中,javascript代码创建了一个新的div元素,并追加到body元素的后面,这其实引发了另一次UI的变化。这意味着,在javascript代码运行过程中,一个新的UI更新任务被加入到队列中,当第二个任务完成后,UI还会再更新一次。

知道了UI线程的工作原理,再回头看上面的例子就不难理解,函数不是在500ms的时候立即执行,而是在500ms的时候加入等待队列。加入等待队列后发现现在线程不是空闲的,因为while500ms的时候还在执行,当while执行结束后,也就是1010ms左右以后,线程才空闲。所以最后的结果是1010ms左右。如图:

 

所以类似如下代码返回什么值就很好理解了

for(var i =1; i<=3; i++ ){
    setTimeout(function(){
        console.log( i )
    },0)
}

 

setTineout类似的还有setInterval,但是setInterval会重复添加javascript任务到队列。当队列中已存在同一个setInterval创建的任务时,后续任务就不会添加到队列中。基于setInterval的设计,会导致两个问题:一个是某些间隔会被跳过;一个是多个定时器的代码执行间隔会比预期的小。看一下下面这段代码:

 

document.getElementById("btn").onclick = function(){
    var start = new Date()
    console.log("开始...")
    setInterval( function(){
        var start = new Date()
        console.log("interval begin")
        while( new Date() - start < 3200 ){}
        console.log( "interval end")
    }, 2000 )
    while( new Date() - start < 3200 ){}
}

 

点击一个button按钮,执行上面的代码,设置了一个200ms间隔的重复定时器,click事件处理程序大约运行了3200ms,定时器代码也大约运行了3200ms。分析如图:

第一个定时器是在2000ms左右加入到队列的,但是这个时候click事件处理程序还没有运行完,等到3200ms左右这个时刻,click事件处理程序运行完,在队列中的第一个定时器任务会进入线程运行。在4000ms左右的时间,第二个定时器任务会进入等待队列。这个时候第一个定时器代码还在运行,并且在6000ms左右的时间第一个定时器代码还是在运行的。这样第二个定时器任务还在等待队列中,所以在6000ms左右,添加不了第三个定时器任务到等待队列。更有问题的是当第一个定时器任务运行结束后,第二个定时器的代码会立即执行。

为了避免setInterval的问题,聪明的前人想到了用setTimeout模拟setInterval。上面的代码可以改写成:

document.getElementById("btn").onclick = function(){
    var start = new Date()
    console.log("开始...")
    setTimeout( function(){
        var start = new Date()
        console.log("interval begin")
        while( new Date() - start < 3200 ){}
        console.log( "interval end")
        setTimeout(arguments.callee, 2000)
    }, 2000 )
    while( new Date() - start < 3200 ){}
}

 

这样做的好处是在本次定时代码执行完以前,不会向等待队列中添加新的定时器。这样在下一次定时器代码执行之前,至少会等待指定的时间间隔。同时因为是setTimeout模拟也不会缺失。

 

 

 

 

posted @ 2014-03-23 10:52  淘小金  阅读(674)  评论(1编辑  收藏  举报