其实这是一篇很久之前就想发的博客了,最近有时间正好整理了下,我们先看一张图:
其中Memory Heap是内存分配的地方,我们这里不谈。Call Stack就是引擎的调用栈,我们都知道js是一个单线程的语言,所以调用栈有且只有一个且只会顺序做某一件事,那么调用栈是什么呢?其实调用栈是一种数据结构,它基本上记录了程序中的位置。如果我们进入函数,我们将它放在堆栈的顶部。如果我们从函数返回,我们会弹出堆栈的顶部。这就是所谓的压栈和出栈~
- 浏览器中的异步
试想一下如果所有的代码都是同步按顺序执行的情况页面将会发生什么?用户点击不能及时响应,请求数据没返回之前页面一直白屏等等。为了解决这一个问题就有了一个叫做Web API的东西,它完全是由浏览器提供。然后有了我们的event-loop和回调队列。如下图:
event-loop的概念大家自行百度,我们这里用一段代码简单的看下loop模型。
function multiply(x,y) {
return x*y
}
function printSquare(x) {
const s = multiply(x,x)
console.log(s)
}
printSquare(5);
这些代码其实是属于一个event-loop的,这次event-loop的调用栈如下图所示:
我们可以知道当代码执行到最后一行的时候本次event-loop完毕,由于没有异步的任务队列,所以还是比较好理解的。
- 堆栈溢出
当一个event-loop执行时调用栈超出本身大小时。比如以下代码:
function fn() {
fn();
};
fn();
- 微任务,宏任务
首先我们要知道什么样的情况会产生微任务:MutationObserver,Promise
宏任务:setTimeout,setInterval,I/O,鼠标事件等
那么宏任务和微任务同样都是异步的任务队列,区别如下:
- 执行时机不同,宏任务在下一次event-loop中,微任务在本次event-loop结束后执行
2.一次event-loop只会执行属于一个宏任务队列,而微任务可以一直执行
我们用几行代码来验证下:
function multiply(x,y) {
return x*y
}
function printSquare(x) {
const s = multiply(x,x)
setTimeout(() => {
console.log('我是另外一个event-loop')
}, 0)
new Promise((resolve, reject) => {
console.log('hah')
resolve(() => {})
}).then(() => {
console.log('heh')
})
new Promise((resolve, reject) => {
console.log('haha')
resolve(() => {})
}).then(() => {
console.log('hehe')
})
}
printSquare(5);
console.log('我是一个event-loop')
打印如下:
我们针对结果来分析下:
在分析之前我们要先明白一个东西,promise虽然是微任务,但是promise的executor部分确是同步执行的,参考MDN:
首先是第一个event-loop:
1.顺序执行两个promise的executor
2.执行最后一行console
第二个event-loop:
1.执行完所有微任务
第三个event-loop:
1.执行setTimeout回调部分
- 如何验证所有的微任务都是在一个event-loop执行,而宏任务不是
其实这个也比较容易验证,比如我们递归调用promise的时候其实是和栈溢出的情况一样的,但是当我们如下调用的时候:
function printSquare() {
console.log('我是另外一个event-loop')
setTimeout(() => {
printSquare()
}, 0)
}
printSquare();
浏览器是不会栈溢出的~
- web-worker
最后我们谈一下web-worker,web-worker和异步的模型不太一样。因为根据以上我们知道虽然我们有event-loop,有队列,但是执行栈始终都只有一个,异步的队列最终也是在调用栈中执行的,调用栈中如果调用的一个函数处理了一段很复杂的逻辑就会影响下一次用户的人机交互,那么这时候web-worker就派上用场了,大家可以把这种庞大的运算逻辑放在web-worker中处理,比如图片算法对比等等。这时候就不会一直占用当前调用栈了~