其实,大家都知道Javascript的语言执行环境是单线程的,浏览器无论在什么时候都有且只有一个线程在运行Javascript程序。那Ajax发送异步请求怎么解释,setTimeout/setInterval定时执行回调函数又是怎么解释呢?
要说解释清楚这些问题,还得从浏览器内核处理定时器(setTimeout、setInterval)和响应浏览器事件说起。
浏览器内核允许多个线程异步执行,这些线程在内核制控下相互配合以保持同步。浏览器内核的实现至少有三个常驻线程:javascript引擎线程、GUI渲染线程、浏览器事件触发线程。除些以外,也有一些执行完就终止的线程:如Http请求线程等,这些异步线程都会产生不同的异步事件。
我们可以通过下面这张图来理解JavaScript引擎与另外那些线程之间的通信机制。
<!DOCTYPE HTML> <html> <head> <meta charset="utf-8" > <title>测试</title> <style> *{margin:0;padding:0} body{background-color:#000000; color: #ffffff} </style> </head> <body> <script type="text/javascript"> setTimeout(function(){ while(true){ console.log("Do first CallBack"); } },1000); setInterval(function(){ console.log("Do second CallBack_____"+new Date().getTime()); },2000); </script> </body> </html>
执行结果告诉我们,setInterval的回调函数一直没有被执行,在执行setTimeout回调函数是发生阻塞,导致执行队列后面的回调函数无法执行。
假如代码的调用方式是 setTimeout(func, 100)
,那么该代码被执行 100 毫秒之后,定时器的事件被触发。如果这个时候 JavaScript 引擎没有正在执行的其它代码,则与此定时器对应的 JavaScript 方法 func
就可以被执行。否则的话,该 JavaScript 方法的执行就被加入到等待的队列中。当 JavaScript 引擎空闲的时候,会从这个队列中选择一个等待的 JavaScript 方法来执行。也就是说,虽然在调用 setTimeout()
的时候设置的间隔时间是 100 毫秒,与之对应的 JavaScript 方法实际被执行的间隔有可能大于设定的 100 毫秒,取决于是否有其它代码正在被执行和执行所花费的时间。因此 setTimeout()
的实际生效的间隔时间可能大于设定的时间。
而 setInterval()
的执行方式与 setTimeout()
有很大不同。假如代码的调用方式是 setInterval(func, 100)
,则每隔 100 毫秒,定时器的事件就会被触发。与 setTimeout()
相同的是,如果当前 JavaScript 引擎空闲的话,则定时器对应的方法 func
会被立即执行。否则的话,该 JavaScript 方法的执行就会被加入到等待队列中。由于定时器的事件是每隔 100 毫秒就触发一次,有可能某一次事件触发的时候,上一次事件的处理方法还没有机会得到执行,仍然在等待队列中。这个时候,这个新的定时器事件就被丢弃。需要注意的是,由于 JavaScript 引擎的这种执行方式,有可能定时器事件处理方法的两次被执行的实际时间间隔小于设定的间隔。比如上一个定时器事件的处理方法触发之后,等待了 50 毫秒才获得被执行的机会。而第二个定时器事件的处理方法被触发之后,马上就被执行了。那么这两者之间的时间间隔实际上只有 50 毫秒。因此,setInterval()
并不适合实现精确的按固定间隔的调度操作。
总的来说,使用 setTimeout()
和 setInterval()
的时候,都不能满足精确的时间间隔。通过 setTimeout()
设置的 JavaScript 方法的实际执行间隔时间不小于设定的时间,而通过 setInterval()
设置的重复执行的 JavaScript 方法的间隔可能会小于设定的时间。
推荐一篇博客:How Javascript Timers Work.