话不多说先看代码来引出今天的问题
//下面两个定时器的输出的先后顺序是啥呢?
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执行机制
- 众所周知 js是单线程的:同一时间只能做一件事。记住这个很重要,虽然上面说了3中异步的线程,但是他们做的也只是把对应的事件做下处理,然后推给主线程来执行,而主线程是单线程的同一时间只做一件事情,多余事情就排队吧!!!!很重要
- 看图说话,看看js执行流程
导图解读: (注意:最顶端任务进入执行栈,栈:先进后出,后进先出) js任务中无非为同步任何和异步任务2中。在任务进入执行栈后,同步和异步任务分别进入不同的执行“场所”,同步任务进入主线程,异步任务进入Event Table 并注册函数。当指定的事情完成时(比如:定时器的延迟时间到了,ajax请求的数据发回来了,触发了回调函数,dom事件被用户触发) ,Event Table 会将这个函数移入 Event Queue(事件队列) 并注册回调函数
当主线程的任务执行完毕后, 主线程为空时,就会去Event Queue 看看,如果有则读取队列里的函数,并将它放入主线程中执行(而进入Event Queue 的先后顺序,也是被主线程抓取的顺序) 。上述过程会不断重复,这就是 Event Loop (事件循环/事件轮循) 再来看看同步任务具体执行的过程
function foo(){ function bar(){ console.log("bar"); } bar(); console.log("foo"); } foo();复制代码
我们来具体看看上面的执行过程
- 代码没有执行的时候,执行栈为空栈
- foo函数执行时,创建了一帧,这帧包含了形参、局部变量(预编译过程),然后把这一帧压入栈中
- 执行foo函数内代码,执行bar函数
- 创建新帧,同样有形参、局部变量,压入栈中
- bar函数执行完毕,输出bar,弹出栈
- foo函数执行完毕,输出foo,弹出栈(可能有小伙伴会说,那把console.log("foo")放在bar函数的执行的上面。foo函数不就先执行完了嘛? 即使这样做了,虽然是先输出foo但也是foo函数后执行完,因为在bar函数执行完毕后,如果后面没有代码了,他会隐式的执行一句 return ; 来终止这个函数)
- 执行栈为空
我们再来深入了解下执行栈:
上面代码我们只套了一层函数,如果套多层函数,或者有多个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’);
复制代码
- Ajax 进入Event Table ,并注册函数;
- ajax事件完成,http网络请求线程 注册回调函数success,并放入Event Queue(任务队列)中等待 主线程(执行栈)读取任务
- 主线程读取 success函数并执行,console.log(data);
5.换一张图继续理解
对2 做一点补充:
细心的朋友已经发行,我在上面写 主线程的时候()里面写了一个调用栈。没错 执行栈其实相当于js主线程。我的个人理解,js单线程执行是,遇到同步的代码,从上到下依次(预编译的问题另说),遇到异步的代码就一脚踢开,让该管异步代码的去管理(参考第一点浏览器常驻线程)。等同步代码执行完毕之后,再去看看Event Queue(任务队列)里面看看有没有,可以执行的代码(回调,定时器,事件),有就拿过来执行,没有就一会再来看看(这个事件特别短,也可能是有专门的触发机制,总的就是 只有执行栈为空,Event Queue里面有任务就会马上拿来执行)
三:问题的解决
好了,说到这里,就可以回头来看看我们最开始抛出的问题:
对上面代码的分析:
- 遇到setTimeout(fn,200) 一脚踢开,让定时器触发线程去管理,在一边面壁思过的数数,数够了200ms,就推入Event Queue中;
- for循环 ,就一直执行,直到执行完毕再往下走
- 遇到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岁,在校大四学习。坐标成都,希望能找个前端的正式岗或者实习岗工作。如果有招人或者内推的大佬,可以留言细聊哟。