异步编程(一):V8是如何实现微任务的?
宏任务和微任务
宏任务很简单,就是指消息队列中的等待被主线程执行的事件。
微任务稍微复杂一点,其实你可以把微任务看成是一个需要异步执行的函数,执行时机是在主函数执行结束之后、当前宏任务结束之前。
主线程、调用栈、消息队列
调用栈是一种数据结构,用来管理在主线程上执行的函数的调用关系。
function bar() {
}
foo(fun){
fun()
}
foo(bar)
当V8准备执行这段代码时,会先将全局执行上下文压入到调用栈中,如下图所示:
然后V8便开始在主线程上执行foo函数,首先它会创建foo函数的执行上下文,并将其压入栈中,那么此时调用栈、主线程的关系如下图所示:
然后,foo函数又调用了bar函数,那么当V8执行bar函数时,同样要创建bar函数的执行上下文,并将其压入栈中,最终效果如下图所示:
首先,主线程会从消息队列中取出需要执行的宏任务,假设当前取出的任务就是要执行的这段代码,这时候主线程便会进入代码的执行状态。这时关于主线程、消息队列、调用栈的关系如下图所示:
接下来V8就要执行foo函数了,同样执行foo函数时,会创建foo函数的执行上下文,并将其压入栈中,最终效果如下图所示:
当V8执行执行foo函数中的setTimeout时,setTimeout会将foo函数封装成一个新的宏任务,并将其添加到消息队列中,在V8执行setTimeout函数时的状态图如下所示:
等foo函数执行结束,V8就会结束当前的宏任务,调用栈也会被清空,调用栈被清空后状态如下图所示:
当一个宏任务执行结束之后,忙碌的主线程依然不会闲下来,它会一直重复这个取宏任务、执行宏任务的过程。刚才通过setTimeout封装的回调宏任务,也会在某一时刻被主线取出并执行,这个执行过程,就是foo函数的调用过程。具体示意图如下所示:
微任务解决了宏任务执行时机不可控的问题
不过,对于栈溢出问题,虽然我们可以通过将某些函数封装成宏任务的方式来解决,但是宏任务需要先被放到消息队列中,如果某些宏任务的执行时间过久,那么就会影响到消息队列后面的宏任务的执行,而且这个影响是不可控的,因为你无法知道前面的宏任务需要多久才能执行完成。
微任务
理解微任务的执行时机,你只需要记住以下两点:
首先,如果当前的任务中产生了一个微任务,通过Promise.resolve()或者Promise.reject()都会触发微任务,触发的微任务不会在当前的函数中被执行,所以执行微任务时,不会导致栈的无限扩张;
其次,和异步调用不同,微任务依然会在当前任务执行结束之前被执行,这也就意味着在当前微任务执行结束之前,消息队列中的其他任务是不可能被执行的。
因此在函数内部触发的微任务,一定比在函数内部触发的宏任务要优先执行。
解析code执行过程:
能否在微任务中循环地触发新的微任务?
既然宏任务和微任务都是异步调用,只是执行的时机不同,那能不能在setTimeout解决栈溢出的问题时,把触发宏任务改成是触发微任务呢?
function foo() {
return Promise.resolve().then(foo)
}
foo()
当执行foo函数时,由于foo函数中调用了Promise.resolve(),这会触发一个微任务,那么此时,V8会将该微任务添加进微任务队列中,退出当前foo函数的执行。
然后,V8在准备退出当前的宏任务之前,会检查微任务队列,发现微任务队列中有一个微任务,于是先执行微任务。由于这个微任务就是调用foo函数本身,所以在执行微任务的过程中,需要继续调用foo函数,在执行foo函数的过程中,又会触发了同样的微任务。
那么这个循环会一直下去,当前的宏任务无法推出,也就意味着消息队列中其他的宏任务是无法被执行的,比如通过鼠标、键盘所产生的事件。这些事件会一直保存在消息队列中,页面无法响应这些事件,具体的体现就是页面的卡死。
不过,由于V8每次执行微任务时,都会退出当前foo函数的调用栈,所以这段代码是不会造成栈溢出的。
此文章为5月Day24学习笔记,内容来源于极客时间《图解 Google V8》,日拱一卒,每天进步一点点💪💪
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net