JavaScript是一个单线程、非阻塞、异步、解释性脚本语言。单线程的运行环境,使的它有且只有一个调用栈,它每次自能做一件事。
可以把调用栈认为是一个存储函数调用的栈结构,遵循先进后出的原则。
上面这个图的大致过程是这样的,首先我们会有一个main函数,它指代文件本身。printSquare函数被调用了,我们将其压入栈,在printSquare函数中又调用了一个函数squared函数,将其压入栈。在squared函数中我么们又调用了multiply函数,一样的将其压入栈。现在该出栈了,multiply函数返回16后出栈,squared函数同样返回16出栈,printSquare函数在打印输出后也出栈,main函数出栈。
window.onload = () =>{
function foo() {
throw new Error('Error');
}
function bar() {
foo();
}
function baz() {
bar();
}
baz();
}
我们可以看到当执行foo函数时报错,接着在bar中报错,再到baz中报错。
window.onload = () =>{
function foo() {
return foo();
}
foo();
}
让我们来看一个例子
我们给点击函数绑定一个下面的函数
function loop() {
Promise.resolve().then(loop)
}
loop();
点击按钮过后会阻塞页面渲染。因为JavaScript是单线程的,所以会阻塞页面渲染,我们应该如何处理?最简单的就是提供异步回调。于是就有了异步事件的概念,注册一个回调函数,比如说发一个网络请求,我们告诉主程序等到接收到数据后通知我,然后我们就可以去做其他的事情了。
然后在异步完成后,会通知到我们,但是此时可能程序正在做其他的事情,所以即使异步完成了也需要在一旁等待,等到程序空闲下来才有时间去看哪些异步已经完成了,可以去执行。
上一小节我们讲到了什么是调用栈,大家也知道了当我们执行 JS 代码的时候其实就是往执行栈中放入函数,那么遇到异步代码的时候该怎么办?其实当遇到异步的代码时,会被挂起并在需要执行的时候加入到 Task(有多种 Task) 队列中。一旦执行栈为空,Event Loop 就会从 Task 队列中拿出需要执行的代码并放入执行栈中执行,所以本质上来说 JS 中的异步还是同步行为。
这个就像去银行办业务一样,先要取号进行排号。
一般上边都会印着类似:“您的号码为XX,前边还有XX人。”之类的字样。
因为柜员同时职能处理一个来办理业务的客户,这时每一个来办理业务的人就可以认为是银行柜员的一个宏任务来存在的,当柜员处理完当前客户的问题以后,选择接待下一位,广播报号,也就是下一个宏任务的开始。
所以多个宏任务合在一起就可以认为说有一个任务队列在这,里边是当前银行中所有排号的客户。
任务队列中的都是已经完成的异步操作,而不是说注册一个异步任务就会被放在这个任务队列中,就像在银行中排号,如果叫到你的时候你不在,那么你当前的号牌就作废了,柜员会选择直接跳过进行下一个客户的业务处理,等你回来以后还需要重新取号。
而且一个宏任务在执行的过程中,是可以添加一些微任务的,就像在柜台办理业务,你前边的一位老大爷可能在存款,在存款这个业务办理完以后,柜员会问老大爷还有没有其他需要办理的业务,这时老大爷想了一下:“最近P2P爆雷有点儿多,是不是要选择稳一些的理财呢”,然后告诉柜员说,要办一些理财的业务,这时候柜员肯定不能告诉老大爷说:“您再上后边取个号去,重新排队”。
所以本来快轮到你来办理业务,会因为老大爷临时添加的“理财业务”而往后推。
也许老大爷在办完理财以后还想 再办一个信用卡**?或者 再买点儿纪念币?
无论是什么需求,只要是柜员能够帮她办理的,都会在处理你的业务之前来做这些事情,这些都可以认为是微任务。
这就说明:你大爷永远是你大爷
在当前的微任务没有执行完成时,是不会执行下一个宏任务的。
所以就有了那个经常在面试题、各种博客中的代码片段:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
})
console.log(2)
setTimeout就是作为宏任务来存在的,而Promise.then则是具有代表性的微任务,上述代码的执行顺序就是按照序号来输出的。
所有会进入的异步都是指的事件回调中的那部分代码
也就是说new Promise在实例化的过程中所执行的代码都是同步进行的,而then中注册的回调才是异步执行的。
在同步代码执行完成后才回去检查是否有异步任务完成,并执行对应的回调,而微任务又会在宏任务之前执行。
所以就得到了上述的输出结论1、2、3、4。
+部分表示同步执行的代码
+setTimeout(_ => {
- console.log(4)
+})
+new Promise(resolve => {
+ resolve()
+ console.log(1)
+}).then(_ => {
- console.log(3)
+})
+console.log(2)
本来setTimeout已经先设置了定时器(相当于取号),然后在当前进程中又添加了一些Promise的处理(临时添加业务)。
所以进阶的,即便我们继续在Promise中实例化Promise,其输出依然会早于setTimeout的宏任务:
setTimeout(_ => console.log(4))
new Promise(resolve => {
resolve()
console.log(1)
}).then(_ => {
console.log(3)
Promise.resolve().then(_ => {
console.log('before timeout')
}).then(_ => {
Promise.resolve().then(_ => {
console.log('also before timeout')
})
})
})
console.log(2)
当然了,实际情况下很少会有简单的这么调用Promise的,一般都会在里边有其他的异步操作,比如fetch、fs.readFile之类的操作。
宏任务包括 script , setTimeout ,setInterval ,setImmediate ,I/O ,UI rendering。
微任务包括 process.nextTick ,promise ,MutationObserver。
其中task为宏任务,microtask为微任务。
步骤
1. 首先我们从Task队列中取出一个任务按照其函数的调用顺序放入调用栈。
2. 先执行该任务的同步代码,如果遇到异步代码且是微任务就放入Microtask队列中。
3. 接下来执行该任务的所有微任务
4. 最后执行该任务的所有宏任务中的异步代码。
5. 开始下一次Event loop直到Task队列为空。
这里很多人会有个误区,认为微任务快于宏任务,其实是错误的。因为宏任务中包括了 script ,浏览器会先执行一个宏任务,接下来有异步代码的话才会先执行微任务。