文章目录
1.下面三段代码会执行结果什么不同
function foo() {
foo() // 是否存在堆栈溢出错误?
}
foo()
function foo() {
setTimeout(foo, 0) // 是否存在堆栈溢出错误?
}
function foo() {
return Promise.resolve().then(foo)
}
foo()
A:
- 第一段:V8就会报告 栈溢出的错误
- 第二段:正确执⾏
- 第三段:没有栈溢出的错误,却会造成⻚⾯的卡死
2.为什么第一段会栈溢出
由于foo函数内部嵌套调⽤它⾃⼰,所以在调⽤foo函数的时候,它的栈会⼀直向上增⻓,但是由于栈空间在
内存中是连续的,所以通常我们都会限制调⽤栈的⼤⼩,如果当函数嵌套层数过深时,过多的执⾏上下⽂堆
积在栈中便会导致栈溢出,最终如下图所⽰:
3.为什么第二段会正常
setTimeout的本质是将同步函数调⽤改成异步函数调⽤,
这⾥的异步调⽤是将foo封装成事件,并将其添加进 消息队列中,然后主线程再按照⼀定规则循环地从消息队列中读取下⼀个任务。
⾸先,主线程会从消息队列中取出需要执⾏的宏任务,假设当前取出的任务就是要执⾏的这段代码,这时候
主线程便会进⼊代码的执⾏状态。这时关于主线程、消息队列、调⽤栈的关系如下图所⽰
接下来V8就要执⾏foo函数了,同样执⾏foo函数时,会创建foo函数的执⾏上下⽂,并将其压⼊栈中,最终
效果如下图所⽰:
当V8执⾏执⾏foo函数中的setTimeout时,setTimeout会将foo函数封装成⼀个新的宏任务,并将其添加到
消息队列中,在V8执⾏setTimeout函数时的状态图如下所⽰:
等foo函数执⾏结束,V8就会结束当前的宏任务,调⽤栈也会被清空,调⽤栈被清空后状态如下图所⽰
当⼀个宏任务执⾏结束之后,忙碌的主线程依然不会闲下来,它会⼀直重复这个取宏任务、执⾏宏任务的过
程。刚才通过setTimeout封装的回调宏任务,也会在某⼀时刻被主线取出并执⾏,这个执⾏过程,就是foo
函数的调⽤过程。具体⽰意图如下所⽰:
因为foo函数并不是在当前的⽗函数内部被执⾏的,⽽是封装成了宏任务,并丢进了消息队列中,然后等待
主线程从消息队列中取出该任务,再执⾏该回调函数foo,这样就解决了栈溢出的问题。
4.为什么第三段会卡住页面
理解微任务的执⾏时机,你只需要记住以下两点:
- ⾸先,如果当前的任务中产⽣了⼀个微任务,通过Promise.resolve()或者Promise.reject()都会触发微任务,触发的微任务不会在当前的函数中被执⾏,所以执⾏微任务时,不会导致栈的⽆限扩张;
- 其次,和异步调⽤不同,微任务依然会在当前任务执⾏结束之前被执⾏,这也就意味着在当前微任务执⾏结束之前,消息队列中的其他任务是不可能被执⾏的
因此在函数内部触发的微任务,⼀定⽐在函数内部触发的宏任务要优先执⾏。
当执⾏foo函数时,由于foo函数中调⽤了Promise.resolve(),这会触发⼀个微任务,那么此时,V8会将该微任务添加进微任务队列中,退出当前foo函数的执⾏。
然后,V8在准备退出当前的宏任务之前,会检查微任务队列,发现微任务队列中有⼀个微任务,于是先执⾏微任务。由于这个微任务就是调⽤foo函数本⾝,所以在执⾏微任务的过程中,需要继续调⽤foo函数,在执⾏foo函数的过程中,⼜会触发了同样的微任务。
那么这个循环就会⼀直持续下去,当前的宏任务⽆法退出,也就意味着消息队列中其他的宏任务是⽆法被执⾏的,⽐如通过⿏标、键盘所产⽣的事件。