JavaScript是单线程异步执行的,单线程意味着代码在任务队列中会按照顺序一个接一个的执行。异步表明JavaScript代码在任务队列中的顺序并不彻底等同于代码的书写顺序,好比事件绑定、Ajax、setTimeout()等任务的发生时间是“不可被预期”的。html
既然JavaScript是单线程机制,那Ajax为何是异步的?setTimeout()是怎样执行的?promise
在浏览器中,JavaScript引擎是单线程执行的。也就是说,在同一时间内,只能有一段代码被JavaScript引擎执行。页面加载时,JavaScript引擎会顺序执行页面上全部JavaScript代码,优先执行同步代码。而异步代码由事件触发引擎按照“事件发生”的顺序添加到JavaScript引擎的任务队列中,待全部同步代码执行结束后,JavaScript引擎会按照任务队列中的顺序来执行异步代码。浏览器
下面是知乎上的一段回答:多线程
JavaScript引擎是单线程运行的,浏览器不管在何时都只且只有一个线程在运行JavaScript程序。异步
浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器至少实现三个常驻线程:JavaScript引擎线程,GUI渲染线程,浏览器事件触发线程。线程
- JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JavaScript线程在运行JavaScript程序。
- GUI渲染线程负责渲染浏览器界面,当界面须要重绘(Repaint)或因为某种操做引起回流(Reflow)时,该线程就会执行。但须要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时当即被执行。
- 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其余线程如鼠标点击、Ajax异步请求等,但因为JavaScript的单线程关系全部这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。
了解JavaScript单线程异步执行的机制之后,再来看一看setTimeout()与setInterval()在执行时候的具体状况。htm
JavaScript引擎在执行setTimeout(fn, 10)时,一方面继续执行setTimeout(fn, 10)后面的同步代码,同时另外一方面开始计时,在10ms以后将fn插入任务队列中。待全部同步代码执行结束后(JavaScript引擎空闲),依次任务队列中的异步代码。因此,setTimeout(fn, 10)并不能准确的在10ms以后执行,而是大于等于10ms。blog
看下面两段代码,会对setTimeout()的执行顺序有更直观的印象。队列
第一段:事件
console.log(1) setTimeout(function () {console.log('a')}, 10); setTimeout(function () {console.log('b')}, 0); var sum = 0; for (var i = 0; i < 1000000; i ++) { sum += i; } console.log(sum); setTimeout(function () {console.log('c');}, 0);
输出结果:
代码执行的逻辑如图所示,纵向表明时间,左边表示同步代码的执行顺序,右边表示异步代码的任务队列,从左到后的箭头表示将异步代码插入任务队列。
第二段,将for循环上限去掉一个0:
console.log(1) setTimeout(function () {console.log('a')}, 10); setTimeout(function () {console.log('b')}, 0); var sum = 0; for (var i = 0; i < 100000; i ++) { sum += i; } console.log(sum); setTimeout(function () {console.log('c');}, 0);
输出结果:
两段代码的区别在于for循环执行的时间不一样,第一段代码的for循环执行时间大于10ms,因此console.log('a')先被插入任务队列,等for循环执行结束后,console.log('c')才被插入任务队列。第二段代码的for循环执行时间小于10ms,因此console.log('c')先被插入任务队列。
setInterval()的执行方式与setTimeout()有不一样。假如执行setInterval(fn, 10),则每隔10ms,定时器的事件就会被触发。与setTimeout()相同的是,若是当前没有同步代码在执行(JavaScript引擎空闲),则定时器对应的方法fn会被当即执行,不然,fn就会被加入到任务队列中。因为定时器的事件是每隔10ms就触发一次,有可能某一次事件触发的时候,上一次事件的处理方法fn尚未机会获得执行,仍然在等待队列中,这个时候,这个新的定时器事件就被丢弃,继续开始下一次计时。须要注意的是,因为JavaScript引擎这种单线程异步的执行方式,有可能两次fn的实际执行时间间隔小于设定的时间间隔。好比上一个定时器事件的处理方法触发以后,等待了5ms才得到被执行的机会。而第二个定时器事件的处理方法被触发以后,立刻就被执行了。那么这二者之间的时间间隔实际上只有5ms。所以,setInterval()并不适合实现精确的按固定间隔的调度操做。
下面代码说明了这个问题:
console.log(1) var interval = setInterval(function () { var date = new Date(); console.log(date.getMinutes() + ':' + date.getSeconds() + ':' + date.getMilliseconds()); }, 10); var sum = 0; for (var i = 0; i < 1000000; i ++) { sum += i; } console.log(2); // 清除定时器,避免卡死浏览器 setTimeout(function () { clearInterval(interval); }, 100);
输出结果:
能够看出,setInterval()前两次的间隔时间只有4ms。由于setInterval()第一次被触发后,里面的方法并无立刻被执行,而是等待同步代码执行结束后才被执行,这个过程用了6ms。因此当第一次方法执行事后4ms,第二次方法也被执行了。从setInterval()第二次被触发开始,后面几回的执行都没有被阻塞,因此间隔时间都在11ms左右。
总的来讲,setTimeout()和setInterval()都不能知足精确的时间间隔。假如设定的时间间隔为10ms,则setTimeout(fn, 10)中的fn执行的时间间隔可能大于10ms,而setInterval(fn, 10)中fn执行的时间间隔可能小于10ms。