js事件循环

​ JavaScript异步编程这块儿我的路线是,先看到settimeout的在for循环里面的神奇执行结果,之后学到node开始逐渐了解到有js异步编程这么一说,就了解到了JavaScript语言和其他编程语言在处理异步问题的时候不同寻常的套路,然后就开始研究一点解决这种异步问题的办法——Promise,async,await和生成器这些用法。当然,最终的归宿就在js事件循环这块。趁着给工作室做一次分享会的时候,我也比较详细的去了解了下js事件循环相关的知识。总结成了这篇博客。很多东西都是我通过结合一些个人认为比较好的博客写的,同时融入了自己的代码事件。生命在于折腾~~,哈哈。现在进入正题:

引入

首先先上下面的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
console.log('1');
setTimeout(()=>{
console.log(2)
setTimeout(()=>{
console.log(6)
},0)
},0)
setTimeout(()=>{
console.log(3)
},1000)
setTimeout(()=>{
console.log(4)
},0)

​ 这段代码的执行结果应该是1,2,4,6,3。可能初学异步编程的人会认为结果是1,2,4,3,6。结果的原因之后将会提到,那么在这里可以看见,我们在settimeout中嵌套了一层,同时在外边还执行了不同延时的settimeout来模拟我们的异步请求。多重的异步嵌套确实使人纠缠不清,但如果掌握了事件循环的机制,实际上这个分析起来也不那么难。

相关的概念

​ 首先我们需要知道在事件循环中,宏事件和微事件两个概念:

​ 在一个线程中,事件循环是唯一的,但是任务队列可以有多个,同时任务队列分为宏任务和微任务两种,最新的版本中被称为task和jobs。两个大致包括下面的内容:

  • 宏任务:script(整体代码), setTimeout, setInterval, setImmediate, I/O, UI rendering
  • 微任务:process.nextTick, Promise, MutationObserver(html5新特性)

​ 事件循环是指,JavaScript代码从script全局(整体代码)开始执行第一次的循环。完毕后进入函数调用栈,直到这个栈中的所有可执行的宏任务被执行万,就进行所有的微任务。等到所有的微任务被执行完。循环再次从宏任务开始执行,然后又是微任务,循环往复这样执行。这里无论是宏任务还是微任务,都是利用函数调用栈来实现的。

​ 接下来我们通过几个例子来说明这个过程。

几个实例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(function(){
console.log(1)
Promise.resolve().then(function(){
console.log(2)
});
},0)

setTimeout(function(){
console.log(3)
},0)
Promise.resolve().then(function(){
console.log(4)
})
console.log(5)

​ 这段代码的输出结果是5、4、1、3、2.分析过程如下:

  1. 首先程序先进入宏任务script全局代码运行,这里程序进入之后,首先是一个settimeout异步的宏任务,将其中的回调函数放在宏任务的队列中等待。

  2. script程序继续执行到下一个settimeout,这里又将其放入在异步宏任务队列中,然后继续执行。

  3. script程序继续执行,接下来是一个Promise。Promise中的设置是一个同步事件,then和catch是异步的。因此这里将then中的函数放在微任务队列中等待。

  4. script程序执行到输出5,之后这一轮同步事件已经进行完毕了。此时任务队列的分布情况如下所示:

    你想要输入的替代文字

  5. 现在执行栈为空,因此按照之前我们所说的原则,现在从微任务中取出一个放入执行栈中执行,所以这个时候,在Promise then中的输出语句开始执行。完毕后出栈,这时候微任务队列已经空了,全部执行完毕。因此又开始从宏任务中取。

    你想要输入的替代文字

  6. 首先按照入队列的顺序从settimeout中取1的回调函数输出,这时候又碰见了Promise,就将其then放在了微任务队列中去。

你想要输入的替代文字

  1. 之后又取2执行,直到宏任务队列为空为止。这时候宏任务队列为空了。然后又转向微任务中去执行。

  2. 之后又开始执行微任务中的程序,输出后清理执行栈。此时微任务和宏任务队列都是空。因此最终输出为5,4,1,3,2

​ 上面这个例子知识简单的将整个事件循环的过程演示了一遍。但是实际上这里还有很多需要注意的细节问题。这些问题也集中突出了JavaScript语言的运行本质。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
process.nextTick(function() {
console.log('immediate2_nextTick');
})
console.log('1');
setTimeout(()=>{
console.log(2)
setTimeout(()=>{
console.log(6)
},0)
},0)
setTimeout(()=>{
console.log(3)
},1000)
setTimeout(()=>{
console.log(4)
},0)

​ 这段代码的输出结果应该是1,immediate2_nextTick,2,4,6,3。这段代码告诉我们js的一个实质问题就是:JavaScript的运行是一个非阻塞的。虽然按照我们之前的说法,js要在执行完一轮宏任务结束之后,才会执行下一轮微任务。但是这里有个条件就是,执行过程中不能有等待。就像上面这个程序中的settimeout输出3一样,虽然看似他论轮数是要早于内层的settimeout的,但是由于它有10000毫秒的等待时间,这是对js单进程的阻塞。js就只能继续先往后执行。等到js执行完毕之后,才会去理会这个延时的回调。js可以看成是一个闲不下来的人,始终要保持自己当前有任务可进行不能等待。实际上这里我认为也可以这样理解,在1000毫秒没有进行完时,这个事件还不能加入到宏任务中去。只有到了1000毫秒响应结束之后才会将其加入到宏任务队列中去。

0%