JavaScript异步编程这块儿我的路线是,先看到settimeout的在for循环里面的神奇执行结果,之后学到node开始逐渐了解到有js异步编程这么一说,就了解到了JavaScript语言和其他编程语言在处理异步问题的时候不同寻常的套路,然后就开始研究一点解决这种异步问题的办法——Promise,async,await和生成器这些用法。当然,最终的归宿就在js事件循环这块。趁着给工作室做一次分享会的时候,我也比较详细的去了解了下js事件循环相关的知识。总结成了这篇博客。很多东西都是我通过结合一些个人认为比较好的博客写的,同时融入了自己的代码事件。生命在于折腾~~,哈哈。现在进入正题:
引入
首先先上下面的这段代码:
1 | console.log('1'); |
这段代码的执行结果应该是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 | setTimeout(function(){ |
这段代码的输出结果是5、4、1、3、2.分析过程如下:
首先程序先进入宏任务script全局代码运行,这里程序进入之后,首先是一个settimeout异步的宏任务,将其中的回调函数放在宏任务的队列中等待。
script程序继续执行到下一个settimeout,这里又将其放入在异步宏任务队列中,然后继续执行。
script程序继续执行,接下来是一个Promise。Promise中的设置是一个同步事件,then和catch是异步的。因此这里将then中的函数放在微任务队列中等待。
script程序执行到输出5,之后这一轮同步事件已经进行完毕了。此时任务队列的分布情况如下所示:
现在执行栈为空,因此按照之前我们所说的原则,现在从微任务中取出一个放入执行栈中执行,所以这个时候,在Promise then中的输出语句开始执行。完毕后出栈,这时候微任务队列已经空了,全部执行完毕。因此又开始从宏任务中取。
首先按照入队列的顺序从settimeout中取1的回调函数输出,这时候又碰见了Promise,就将其then放在了微任务队列中去。
之后又取2执行,直到宏任务队列为空为止。这时候宏任务队列为空了。然后又转向微任务中去执行。
之后又开始执行微任务中的程序,输出后清理执行栈。此时微任务和宏任务队列都是空。因此最终输出为5,4,1,3,2
上面这个例子知识简单的将整个事件循环的过程演示了一遍。但是实际上这里还有很多需要注意的细节问题。这些问题也集中突出了JavaScript语言的运行本质。
1 | process.nextTick(function() { |
这段代码的输出结果应该是1,immediate2_nextTick,2,4,6,3。这段代码告诉我们js的一个实质问题就是:JavaScript的运行是一个非阻塞的。虽然按照我们之前的说法,js要在执行完一轮宏任务结束之后,才会执行下一轮微任务。但是这里有个条件就是,执行过程中不能有等待。就像上面这个程序中的settimeout输出3一样,虽然看似他论轮数是要早于内层的settimeout的,但是由于它有10000毫秒的等待时间,这是对js单进程的阻塞。js就只能继续先往后执行。等到js执行完毕之后,才会去理会这个延时的回调。js可以看成是一个闲不下来的人,始终要保持自己当前有任务可进行不能等待。实际上这里我认为也可以这样理解,在1000毫秒没有进行完时,这个事件还不能加入到宏任务中去。只有到了1000毫秒响应结束之后才会将其加入到宏任务队列中去。