掉坑里出不来系列之JS的Event Loop
本文来简单说一下JavaScript中的事件循环机制,看完还不懂的可以来捶我。
1、关于JavaScript
JavaScript是一门单线程语言,一直以来都没有改变过,以后也不会改变。在最新的HTML5规范中,提出了webworker的多线程概念,但JavaScript是单线程这一核心仍然未变,所谓的多线程都是单线程模拟出来的,所以,一切JavaScript多线程都是纸老虎。
很多人都知道,JavaScript代码是从上到下执行的,所以先来一段代码压压惊:
setTimeout(() => {
console.log('定时器开始啦');
});
new Promise((resolve) => {
resolve();
console.log('准备执行个for循环');
for(let i = 0; i < 3;){ i++;}
}).then(() => {
console.log('执行了then函数');
});
console.log('代码执行结束');
本着JavaScript代码从上到下执行的理念,自信地写下了执行顺序:
//定时器开始啦
//准备执行个for循环
//执行了then函数
//代码执行结束
自信满满地拿去浏览器输出结果,您猜怎么着?上来就整 这么一出,那可真是盖了帽了我的老北鼻:
正确执行顺序:
//准备执行个for循环
//代码执行结束
//执行了then函数
//定时器开始啦
说好的从上到下执行呢... ...
带着疑惑,进入本文的正题,JavaScript的事件循环机制。
2、JavaScript事件循环
JavaScript有一个基于事件循环的并发模型,事件循环负责执行代码、收集和处理事件以及执行队列中的子任务。 ——《MDN》
JavaScript除了广义上的同步任务和异步任务之外,JavaScript对任务还有更精细的定义:
macro-task(宏任务):整体代码script、setTimeout、setInterval
micro-task(微任务):promise、process.nextTick
不同类型的任务会进入到对应的队列中,比如setTimeout和setInterval,都属于一个类型的宏任务,所以会进入到同一个Event Queue。
当我们打开网页的时候,网页的渲染过程就是一大堆的同步任务,比如页面骨架和元素的渲染。而像一些AJAX请求资源等,就是属于异步任务。
下面画个图来描述一下eventloop机制(毕加索之手):
大概解释一下这个图的意思,就是代码执行,会进入到一个js的执行栈中从上而下地执行,当遇到异步任务的时候,会抽出来注册一个回调函数,等回调函数有了结果之后,会被放入到异步队列中,等待主线程空闲的时候去调用,此时,执行栈并没有停止,正在继续往下执行,当主线程为空的时候,js引擎会自动去查询异步队列中是否有等待被调用的函数,如果有则执行,没有则开始下一个循环。在这里,你可能会问,我咋个知道主线程什么时候为空啊?不要慌,js引擎存在一个monitoring process进程,会自动持续地检查主线程执行栈是否为空。
此时,可能有同学就会说:show me the code,碧池
代码来了:
let parmas = {};
axios.get('https://mp.weixin.qq.com',params).then(res => {
console.log('请求成功');
});
console.log('代码执行完了');
上面是一段简易的异步请求伪代码,执行过程大概如下:
·ajax请求进入Event Table,注册回调函数then;
·执行console.log('代码执行完了');
·ajax请求完成,回调函数then进入Event Queue,等待主线程空闲时间去调用;
·主线程从Event Queue读取回调函数then,并执行console.log('请求成功');
3、到这里,你已经对js的执行顺序有了一定的了解,下面说一下万恶的setTimeout:
setTimeout大家对它的印象就是异步、延迟执行,当需要1秒后才执行的时候,通常会这样写:
setTimeout(() => {
console.log('延迟一秒执行');
},1000)
渐渐的setTimeout用的地方多了,问题也慢慢暴露出来了,有时候明明写着延迟3秒执行,实际上却被延迟了5秒甚至10秒,why?
举个栗子:
setTimeout(() => {
console.log('定时器里面');
},2000);
console.log('定时器之外');
根据前面的结论,我们可以知道setTimeout是异步,所以会先执行console.log('定时器之外'),然后再输出console.log('定时器里面')。
去浏览器验证一下结果,正确。下面对这个栗子改造一下:
setTimeout(() => {
task();//伪代码函数
},2000);
sleep(10000);//伪代码,表示这个函数执行了10秒
根据前面的经验,很快得出结果:先执行sleep()函数,2秒后执行task(),但实际并非如此,而是经过了长达10秒的等待,才会执行task()函数。
这时候我们需要重新理解一下setTimeout了。setTimeout并不是说,你定义了多少秒延迟,就会在那个时间点调用,而是要看主线程的拥堵情况,必须等主线程被清空了,才有可能去调用setTimeout,上面改造后的栗子是这样执行的:
·task()函数进入Event Table并注册,定时器开始;
·执行sleep()函数,很慢,持续十秒,定时器仍在继续;
·2秒时间到了,定时器结束,task()函数进入Event Queue,等待主线程的调用,但此时sleep()函数仍在主线程执行着,所以只好等待;
·十秒后,sleep()终于执行完了,主线程被清空,此时,Event Queue中的task()函数被压入到主线程执行;
此时的setTimeout等待时间远超2秒,因为JavaScript是单线程语言,当主线程被占用的时候,异步队列的任务只能在一边等待,所以才会导致setTimeout的实际延迟远大于定义。
setTimeout冷知识:根据HTML标准,setTimeout最低延迟为4毫秒。
setInterval作为setTimeout的孪生兄弟,两者差不多的。唯一需要注意的一点是,对于setInterval(fn,ms)来说,并不是意味着每过ms毫秒就会执行一次fn函数,而是每过ms毫秒,就会有一个fn函数进入Event Queue。一旦setInterval的回调函数fn的执行时间超过了延迟时间ms,那么就完全看不出来有时间间隔。
4、说说promise(process.nextTick就展开了,属于nodejs的内容):
事件循环的循序,决定了js代码的执行顺序。进入整体代码(宏任务)后,开始第一次循环,接着会执行本次循环的所有微任务,执行完毕后再次从宏任务开始循环。
举个栗子:
setTimeout(() => {
console.log('定时器执行');
});
new Promise((resolve) => {
//resolve();
console.log('promise');
}).then(() => {
console.log('then');
})
console.log('代码执行完毕');
这段代码作为宏任务,进入到主线程执行,下面分析一下每一个步骤:
·首先遇到了setTimeout,属于异步任务,所以会将它回调函数注册后压入到宏任务的异步队列中;
·接下来遇到了Promise,new Promise会立即执行,所以会执行console.log('promise'),然后then函数会被分发到微任务队列;
·然后遇到了console.log('代码执行完毕'),立即输出,第一个宏任务到此结束;
·上面我们提到,执行完宏任务后会去查询当前的微任务并执行,所以这里会查找是否有需要被执行的微任务,找到了then函数,但它并没有被调用到,所以不需要执行(当你把上面代码中的注释打开,此时会执行then这个微任务),至此,一次事件循环结束;
·开始下一轮的循环,先查询宏任务,发现了setTimeout,这里会执行console.log('setTimeout');
·结束。
为了方便查看,这里输出一下上面栗子的结果(分两种情况):
//第一种情况,是resolve被注释的时候
promise
console
setTimeout
//第二种情况,是resolve放开的时候
promise
console
then
setTimeout
只要记住两点:JavaScript是一门单线程语言;Event Loop是JavaScript的执行机制。
下面附上一段比较复杂的代码,看看你是否真的掌握了js的执行机制:
console.log('1');
setTimeout(function() {
console.log('2');
process.nextTick(function() {
console.log('3');
})
new Promise(function(resolve) {
console.log('4');
resolve();
}).then(function() {
console.log('5')
})
})
process.nextTick(function() {
console.log('6');
})
new Promise(function(resolve) {
console.log('7');
resolve();
}).then(function() {
console.log('8')
})
setTimeout(function() {
console.log('9');
process.nextTick(function() {
console.log('10');
})
new Promise(function(resolve) {
console.log('11');
resolve();
}).then(function() {
console.log('12')
})
})
具体分为三轮循环:
第一轮循环输出:1,7,6,8(两个同步的宏任务,两个微任务,一次宏+一次微,代表一轮循环);
第二轮循环输出:2,4,3,5(第一个定时器);
第三轮循环输出:9,11,10,12(第二个定时器);
完整输出顺序为:1,7,6,8,2,4,3,5,9,11,10,12。
具体流程按照文章上面的内容慢慢分析,就可以得到正确的答案了,我就不对这个展开详细的讨论了。深夜码字不易,有错别字的谅解一下~~有错误描述的我就懒得改了~
参考文章:https://juejin.im/post/59e85eebf265da430d571f89#heading-1