深度剖析定时器、提一嘴事件轮循

话很少说先看代码来引出今天的问题javascript

//下面两个定时器的输出的前后顺序是啥呢?
setTimeout(function(){            
    console.log("200")        
},200)  
//不了解ES6的朋友,把let 当成var 就好 
for(let i = 0 ; i < 1000 ; i++){            
    console.log('---');        
}        
setTimeout(function(){            
    console.log('0')   
    //实际不可能会是0ms,定时器有一个最低的延时为4ms,形成这个的缘由,我相信聪明的你,
    //确定能在下面的世界轮循机制中找到答案(定时器触发线程和主线程的取出,会有必定的执行时间) 
},0)

//而下面两个定时器的输出结果又是啥呢?
setTimeout(function(){
    console.log("200")
},200)
for(let i= 0; i < 5000 ; i++){
    console.log("---");
}

setTimeout(function(){
    console.log("0")
},0)

复制代码
//上面两个的答案分别是 0  200;    200 0 复制代码

。那么问题来了,第二个定时的delay(延迟时间,如下都用这个单词表示了)明明是 0ms(实际大约4ms,代码中解释了,下面再也不作解释)。第一个定时器的delay 是 200ms,为啥第一个代码正常输出,而第二个代码确实 delay为200ms 的先输出?
前端

如今带着咱们的问题来看看js的事件轮循(Event Loop)机制:java

一:浏览器常驻的线程

  • js引擎线程(解释执行js代码、用户输入、网络请求)
  • GUI线程(绘制用户界面与JS主线程是互斥的。 干了其中一个就不能作另一个)
  • http网络请求线程(处理用户的GET、POST等请求,等返回结果后将回调函数推入任务队列(Evnet Queue))
  • 定时器触发器线程(setTimeout、setInterval等待时间结束后把执行函数推入任务队列中)
  • 浏览器事件处理线程(将click、mouse等交互事件发生后将这些事件放入执行队列中)

二:js执行机制

  1.     众所周知 js是单线程的:同一时间只能作一件事。记住这个很重要,虽然上面说了3中异步的线程,可是他们作的也只是把对应的事件作下处理,而后推给主线程来执行,而主线程是单线程的同一时间只作一件事情,多余事情就排队吧!!!!很重要
  2.     看图说话,看看js执行流程
    导图解读: (注意:最顶端任务进入执行栈,栈:先进后出,后进先出)                                 js任务中无非为同步任何和异步任务2中。在任务进入执行栈后,同步和异步任务分别进入不一样的执行“场所”,同步任务进入主线程,异步任务进入Event Table 并注册函数。
    当指定的事情完成时(好比:定时器的延迟时间到了,ajax请求的数据发回来了,触发了回调函数,dom事件被用户触发) ,Event Table 会将这个函数移入 Event Queue(事件队列) 并注册回调函数
    当主线程的任务执行完毕后, 主线程为空时,就会去Event Queue 看看,若是有则读取队列里的函数,并将它放入主线程中执行(而进入Event Queue 的前后顺序,也是被主线程抓取的顺序) 。上述过程会不断重复,这就是 Event Loop (事件循环/事件轮循)
  3. 再来看看同步任务具体执行的过程ajax

    function foo(){ 
       function bar(){
     console.log("bar");   }
       bar();
       console.log("foo");
    }
    foo();复制代码

      咱们来具体看看上面的执行过程json

  1. 代码没有执行的时候,执行栈为空栈
  2. foo函数执行时,建立了一帧,这帧包含了形参、局部变量(预编译过程),而后把这一帧压入栈中
  3. 执行foo函数内代码,执行bar函数
  4. 建立新帧,一样有形参、局部变量,压入栈中
  5. bar函数执行完毕,输出bar,弹出栈
  6. foo函数执行完毕,输出foo,弹出栈(可能有小伙伴会说,那把console.log("foo")放在bar函数的执行的上面。foo函数不就先执行完了嘛? 即便这样作了,虽然是先输出foo但也是foo函数后执行完,由于在bar函数执行完毕后,若是后面没有代码了,他会隐式的执行一句  return ; 来终止这个函数)
  7. 执行栈为空

  咱们再来深刻了解下执行栈:浏览器

  上面代码咱们只套了一层函数,若是套多层函数,或者有多个bar的同级函数是有区别的。bash

  多层嵌套很简单,就按照上面的流程依次内推就行了,网络

  同级函数则是是重复 3,4,5的步骤。bar执行完毕,弹出栈,bar后面的代码继续执行碰到函数执行则走3,4,5,步骤。dom

4.异步任务具体的执行过程异步

$.ajax({
	url: ‘localhost:/js/demo.json’,
	data: {},
	success: function (data) {
		console.log(data);
	}
});
console.log(‘run’);
复制代码

  1. Ajax 进入Event Table ,并注册函数;
  2. ajax事件完成,http网络请求线程 注册回调函数success,并放入Event Queue(任务队列)中等待 主线程(执行栈)读取任务
  3. 主线程读取 success函数并执行,console.log(data);

5.换一张图继续理解


 对2 作一点补充:

 细心的朋友已经发行,我在上面写 主线程的时候()里面写了一个调用栈。没错 执行栈其实至关于js主线程。个人我的理解,js单线程执行是,遇到同步的代码,从上到下依次(预编译的问题另说),遇到异步的代码就一脚踢开,让该管异步代码的去管理(参考第一点浏览器常驻线程)。等同步代码执行完毕以后,再去看看Event Queue(任务队列)里面看看有没有,能够执行的代码(回调,定时器,事件),有就拿过来执行,没有就一会再来看看(这个事件特别短,也多是有专门的触发机制,总的就是 只有执行栈为空,Event Queue里面有任务就会立刻拿来执行

三:问题的解决

好了,说到这里,就能够回头来看看咱们最开始抛出的问题:

对上面代码的分析:

  1.  遇到setTimeout(fn,200) 一脚踢开,让定时器触发线程去管理,在一边面壁思过的数数,数够了200ms,就推入Event Queue中;
  2. for循环 ,就一直执行,直到执行完毕再往下走
  3. 遇到setTimeout(fn,0) 一脚踢开让,定时器触发线程去管理,在一边面壁思过的数数,数够了200ms,就推入Event Queue中;

 由上面的文字能够分析出,只要for循环的执行时间超过了200ms,第一个定时器就先进入Event Queue中(任务队列,先进先出,后进后出。先进去的就先执行),第二个定时器是在第一个定时器已经进入了Event Queue 以后再触发的,无论他的delay多小也只有后输出。

而for循环的执行时间没有超过200ms时(低于先触发的定时器的delay),for循环执行完毕后,他还在面壁思过的数数,js主线程继续往下走,触发了第二个定时器,依旧一脚踢开,去面壁思过数数,这个时候,只要谁先数完,谁就先进入Event Queue 就先执行 。 上面代码的状况是 delay 为0ms 的先数完,因此先执行,delay为200ms后进入Event Queue 后执行。

四:问题加深

 你觉得这样就完了吗?若是是这样敢说深度剖析定时器?看代码

//表示执行次数的变量        
let count = 0;        /
/开始时间,用来定时的,记录执行的间隔时间        
// + 为一元 '+' 号运算符,将其操做数隐式转换成数字         
let starTime = +new Date();        
function sleep (num){            
    for(let i = 0 ;i < num ; i++){                
    console.log(i);            
    }        
}        
setInterval(function(){            
    count++;            
    console.log(+new Date()  - starTime , count);            
    starTime = +new Date();           
    },1000)  
              
sleep(20000);复制代码

先上执行结果


上面的执行结果除了第二次的都很好解释。第一次执行,时间这么多的缘由是,运行for循环完了以后才能执行定一次的定时器,3以后的就趋于稳定 大概等于delay。

先抛出问题:

    首先主线程一直在运行的时候,setInterval是每到一个delay就往Event Queue推出一个执行函数吗?若是是这样的话,如图所示第一次执行被阻塞的时候为3000 + ,因此能往Evnet Queue里面注册三个定时器,为啥只有第二次的执行间隔时间发生比较大的差距,第三次之后就正常了?  为何 第一次和第二次执行的间隔时间相加总约等于delay的倍数,这是巧合仍是必然?


回答问题:

   咱们先定义一些参数,好方便如下的解释:

   fn1 为定时器的第一次  , fn 2  为定时器的第二次 , fn3 为定时器 第三次和之后的无限次

 关于上面的第一个问题很容易回答, setInterval 确定不是没到一个delay就往Event Queue 推送一个执行函数 ,若是是的话如上代码就会有三个执行函数在任务队列里面了,当主线程执行完毕后,去Event Queue拿函数回去执行会很是快,不可能会出现,fn2,fn3执行间隔这么大。 其实第三个问题才是解题的关键,是仔细想想,什么状况下才能出现这种相加为倍数的状况(好吧,其实怎么想,我也说不清楚)。在试验的过程当中,甚至出现过fn1 的执行间隔为3950 ,fn2的执行间隔为49的状况,当时确实给我形成了很大的悟道,后面经过不断的实验,加询问最终得出告终论


 解决:出现这个事情的缘由是,Event Queue 里面只能存在同一个定时器的一次事件,也就是说在定时器第一次被拿到主线程取走以前,第二次并不会进入Event Queue 。会依旧再Event Table 里面等待。这个等待并非盲目的等待,在每个delay周期都看看Event Queue 里面  上一次 的进去的定时器(fn1) 被主线程取走没有,当取走后,就会在当前delay周期完的时候,把这一次的定时器(fn2)推入 Event Queue ,而这个时候主线程正好没有任务正在执行,主线程就会马上把此次的定时器放入到主线程执行,就形成了,定时器第一次执行和第二次执行的间隔时间相加总等于delay的倍数。 fn3以后的就属于正常状况了,当主线程没有任务,Event Queue 中没有定时器时,就每隔delay执行一次。


5、最后的最后

  第一次写掘金文章(也是第一次写文章),清辩证看待,其中的一些错别字和错误。若是对你有帮助,别忘了点个赞哟。

最后打个广告,本人男,22岁,在校大四学习。坐标成都,但愿能找个前端的正式岗或者实习岗工做。若是有招人或者内推的大佬,能够留言细聊哟。

相关文章
相关标签/搜索