JavaScript异步机制

单线程异步执行的JavaScript

JavaScript是单线程异步执行的,单线程意味着代码在任务队列中会按照顺序一个接一个的执行。异步表明JavaScript代码在任务队列中的顺序并不彻底等同于代码的书写顺序,好比事件绑定、Ajax、setTimeout()等任务的发生时间是“不可被预期”的。html

既然JavaScript是单线程机制,那Ajax为何是异步的?setTimeout()是怎样执行的?promise

在浏览器中,JavaScript引擎是单线程执行的。也就是说,在同一时间内,只能有一段代码被JavaScript引擎执行。页面加载时,JavaScript引擎会顺序执行页面上全部JavaScript代码,优先执行同步代码。而异步代码由事件触发引擎按照“事件发生”的顺序添加到JavaScript引擎的任务队列中,待全部同步代码执行结束后,JavaScript引擎会按照任务队列中的顺序来执行异步代码。浏览器

下面是知乎上的一段回答:多线程

JavaScript引擎是单线程运行的,浏览器不管在何时都只且只有一个线程在运行JavaScript程序。异步

浏览器的内核是多线程的,它们在内核控制下相互配合以保持同步,一个浏览器至少实现三个常驻线程:JavaScript引擎线程GUI渲染线程浏览器事件触发线程线程

  1. JavaScript引擎是基于事件驱动单线程执行的,JavaScript引擎一直等待着任务队列中任务的到来,而后加以处理,浏览器不管何时都只有一个JavaScript线程在运行JavaScript程序。
  2. GUI渲染线程负责渲染浏览器界面,当界面须要重绘(Repaint)或因为某种操做引起回流(Reflow)时,该线程就会执行。但须要注意,GUI渲染线程与JavaScript引擎是互斥的,当JavaScript引擎执行时GUI线程会被挂起,GUI更新会被保存在一个队列中等到JavaScript引擎空闲时当即被执行。
  3. 事件触发线程,当一个事件被触发时该线程会把事件添加到待处理队列的队尾,等待JavaScript引擎的处理。这些事件可来自JavaScript引擎当前执行的代码块如setTimeout、也可来自浏览器内核的其余线程如鼠标点击、Ajax异步请求等,但因为JavaScript的单线程关系全部这些事件都得排队等待JavaScript引擎处理(当线程中没有执行任何同步代码的前提下才会执行异步代码)。

了解JavaScript单线程异步执行的机制之后,再来看一看setTimeout()与setInterval()在执行时候的具体状况。htm

setTimeout()与setInterval()

setTimeout()

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);

输出结果:

promises-1

代码执行的逻辑如图所示,纵向表明时间,左边表示同步代码的执行顺序,右边表示异步代码的任务队列,从左到后的箭头表示将异步代码插入任务队列。

promises-3

第二段,将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);

输出结果:

promises-2

两段代码的区别在于for循环执行的时间不一样,第一段代码的for循环执行时间大于10ms,因此console.log('a')先被插入任务队列,等for循环执行结束后,console.log('c')才被插入任务队列。第二段代码的for循环执行时间小于10ms,因此console.log('c')先被插入任务队列。

promises-4

setInterval()

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);

输出结果:

promises-5

能够看出,setInterval()前两次的间隔时间只有4ms。由于setInterval()第一次被触发后,里面的方法并无立刻被执行,而是等待同步代码执行结束后才被执行,这个过程用了6ms。因此当第一次方法执行事后4ms,第二次方法也被执行了。从setInterval()第二次被触发开始,后面几回的执行都没有被阻塞,因此间隔时间都在11ms左右。

总的来讲,setTimeout()和setInterval()都不能知足精确的时间间隔。假如设定的时间间隔为10ms,则setTimeout(fn, 10)中的fn执行的时间间隔可能大于10ms,而setInterval(fn, 10)中fn执行的时间间隔可能小于10ms。

相关文章
相关标签/搜索