JavaScript 是单线程的,这意味着在任何时候只能有一段代码执行。JavaScript 主线程在运行时,会建立一个执行同步代码的栈和执行异步代码的队列,如下图所示:
JavaScript主线程的栈和队列.png
异步代码的执行时机
JavaScript 主线程在执行时,如果遇到异步的代码,就会将这些代码加入到异步队列中,然后继续执行同步代码栈中的代码。
这里需要注意的是,下面的代码表示的是在 5 秒后被加入异步队列等待执行,而不是加入异步队列 5 秒后执行:
setTimeout(() => console.log("1"),5);
当同步代码栈被清空后,意味着同步代码已经执行完毕,这时就开始执行异步队列中代码。
异步队列中的代码在执行时,会将其的回调函数和相关的函数调用放到同步代码栈中去执行。当同步代码栈被清空,意味着当前的异步任务已经执行完毕,然后从异步队列中取下一个任务执行,循环往复。
以上就是一个基本的 JavaScript 并发模型。
一段经典的代码
以下是一段面试中经常考察的一段经典代码,你可以试着推敲一下打印结果:
// 请写出下面代码的输出结果
setTimeout(() => {
console.log(1)
},0);
console.log(2);
new Promise((res) => {
console.log(3)
res()
console.log(4)
}).then(()=>{
console.log(5)
}).then(() => {
console.log(6)
})
console.log(7)
这段代码的正确打印结果为:
2
3
4
7
5
6
1
从上面的输出结果大概可以看出:同步代码总是优先于异步代码执行的,而对于异步代码的执行,似乎有个优先顺序,这其实和异步队列的实现有关,我们可以再深入了解一下。
Macro Tasks 和 Micro Tasks
在上面的打印结果中,Promise 在 resolve 后先于 setTimeout 执行,说明 Promise 任务的优先级比 setTimeout 任务的优先级要高。这就引出了两个概念:Macro Tasks(宏任务)和 Micro Tasks(微任务)。
JavaScript 的异步队列实际上又被划分为两个小队列:宏任务队列和微任务队列。宏任务队列中包含了以下异步任务:
- setTimeout
- setInterval
- setImmediate
- UI 事件
- Ajax
微任务队列中包含了以下异步任务:
- Promise
- process.nextTick
- Object.observe(已废弃)
- MutationObserver(可用来监听 DOM 变化)
一般来说,宏任务的开销比微任务的开销要大。
它们的关系如图所示:
异步队列.png
Macro Tasks 和 Micro Tasks 的执行顺序
Macro Tasks 和 Micro Tasks 的执行顺序如下:在执行每个 Macro Task(宏任务)之前,会先检查有微任务队列中有没有任务需要处理,若有,就先将微任务队列中的任务全部放入同步执行栈中执行,直到微任务队列被清空,然后再执行宏任务队列中的任务,循环往复。
因此,这就能解释为什么 Promise 会优先于 setTimeout 执行,即使 Promise 的执行链很长。这是因为 setTimeout 属于 Macro Task,而 Promise 属于 Micro Task,在执行 Macro Task 之前需要先将 Micro Task 队列清空。如下面的代码:
setTimeout(()=>{
console.log(1)
},0)
new Promise((res) => {
res()
}).then(() => {
console.log(4)
}).then(() =>{
console.log(5)
}).then(() =>{
console.log(6)
}).then(() =>{
console.log(7)
}).then(() =>{
console.log(8)
}).then(() =>{
console.log(9)
}).then(() =>{
console.log(10)
})
输出结果为:
4
5
6
7
8
9
10
1
需要注意的是:新建 Promise 对象时需要传入一个函数参数,这个函数参数是同步代码,如下例子:
new Promise(() => {
console.log(1)
});
console.log(2)
打印结果为:
1
2
总结
本文简单介绍了 JavaScript 的并发模型,或者说 JavaScript 代码的执行顺序。这里做一个总结:
- JavaScript 主线程在运行时,如果发现异步方法,会将它们放入异步队列中,同步方法则放入同步执行栈中依次执行
- 异步队列中的代码只有在同步执行栈被清空后才有机会执行
- 异步队列又分为 Macro Tasks 队列(宏任务队列)和 Micro Tasks 队列(微任务队列),在执行每一个 Macro Task 之前,总是会先执行 Micro Tasks 队列中的代码(若有),当 Micro Tasks 被清空之后,再去执行 Macro Task。
附:参考资料
JavaScript并发模型与Event Loop
HTML系列:macrotask和microtask
MutationObserver
Navigator.sendBeacon
完。
网友评论