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

1,949 阅读10分钟

话不多说先看代码来引出今天的问题

//下面两个定时器的输出的先后顺序是啥呢?
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)机制:

一:浏览器常驻的线程

  • 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. 再来看看同步任务具体执行的过程

    function foo(){ 
       function bar(){
     console.log("bar");   }
       bar();
       console.log("foo");
    }
    foo();

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

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

  我们再来深入了解下执行栈:

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

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

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

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执行一次。


五、最后的最后

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

最后打个广告,本人男,22岁,在校大四学习。坐标成都,希望能找个前端的正式岗或者实习岗工作。如果有招人或者内推的大佬,可以留言细聊哟。