JavaScript 是單線程的,這意味著在任何時(shí)候只能有一段代碼執(zhí)行。JavaScript 主線程在運(yùn)行時(shí),會(huì)建立一個(gè)執(zhí)行同步代碼的棧和執(zhí)行異步代碼的隊(duì)列,如下圖所示:

異步代碼的執(zhí)行時(shí)機(jī)
JavaScript 主線程在執(zhí)行時(shí),如果遇到異步的代碼,就會(huì)將這些代碼加入到異步隊(duì)列中,然后繼續(xù)執(zhí)行同步代碼棧中的代碼。
這里需要注意的是,下面的代碼表示的是在 5 秒后被加入異步隊(duì)列等待執(zhí)行,而不是加入異步隊(duì)列 5 秒后執(zhí)行:
setTimeout(() => console.log("1"),5);
當(dāng)同步代碼棧被清空后,意味著同步代碼已經(jīng)執(zhí)行完畢,這時(shí)就開始執(zhí)行異步隊(duì)列中代碼。
異步隊(duì)列中的代碼在執(zhí)行時(shí),會(huì)將其的回調(diào)函數(shù)和相關(guān)的函數(shù)調(diào)用放到同步代碼棧中去執(zhí)行。當(dāng)同步代碼棧被清空,意味著當(dāng)前的異步任務(wù)已經(jīng)執(zhí)行完畢,然后從異步隊(duì)列中取下一個(gè)任務(wù)執(zhí)行,循環(huán)往復(fù)。
以上就是一個(gè)基本的 JavaScript 并發(fā)模型。
一段經(jīng)典的代碼
以下是一段面試中經(jīng)??疾斓囊欢谓?jīng)典代碼,你可以試著推敲一下打印結(jié)果:
// 請(qǐng)寫出下面代碼的輸出結(jié)果
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)
這段代碼的正確打印結(jié)果為:
2
3
4
7
5
6
1
從上面的輸出結(jié)果大概可以看出:同步代碼總是優(yōu)先于異步代碼執(zhí)行的,而對(duì)于異步代碼的執(zhí)行,似乎有個(gè)優(yōu)先順序,這其實(shí)和異步隊(duì)列的實(shí)現(xiàn)有關(guān),我們可以再深入了解一下。
Macro Tasks 和 Micro Tasks
在上面的打印結(jié)果中,Promise 在 resolve 后先于 setTimeout 執(zhí)行,說明 Promise 任務(wù)的優(yōu)先級(jí)比 setTimeout 任務(wù)的優(yōu)先級(jí)要高。這就引出了兩個(gè)概念:Macro Tasks(宏任務(wù))和 Micro Tasks(微任務(wù))。
JavaScript 的異步隊(duì)列實(shí)際上又被劃分為兩個(gè)小隊(duì)列:宏任務(wù)隊(duì)列和微任務(wù)隊(duì)列。宏任務(wù)隊(duì)列中包含了以下異步任務(wù):
- setTimeout
- setInterval
- setImmediate
- UI 事件
- Ajax
微任務(wù)隊(duì)列中包含了以下異步任務(wù):
- Promise
- process.nextTick
- Object.observe(已廢棄)
- MutationObserver(可用來監(jiān)聽 DOM 變化)
一般來說,宏任務(wù)的開銷比微任務(wù)的開銷要大。
它們的關(guān)系如圖所示:

Macro Tasks 和 Micro Tasks 的執(zhí)行順序
Macro Tasks 和 Micro Tasks 的執(zhí)行順序如下:在執(zhí)行每個(gè) Macro Task(宏任務(wù))之前,會(huì)先檢查有微任務(wù)隊(duì)列中有沒有任務(wù)需要處理,若有,就先將微任務(wù)隊(duì)列中的任務(wù)全部放入同步執(zhí)行棧中執(zhí)行,直到微任務(wù)隊(duì)列被清空,然后再執(zhí)行宏任務(wù)隊(duì)列中的任務(wù),循環(huán)往復(fù)。
因此,這就能解釋為什么 Promise 會(huì)優(yōu)先于 setTimeout 執(zhí)行,即使 Promise 的執(zhí)行鏈很長(zhǎng)。這是因?yàn)?setTimeout 屬于 Macro Task,而 Promise 屬于 Micro Task,在執(zhí)行 Macro Task 之前需要先將 Micro Task 隊(duì)列清空。如下面的代碼:
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)
})
輸出結(jié)果為:
4
5
6
7
8
9
10
1
需要注意的是:新建 Promise 對(duì)象時(shí)需要傳入一個(gè)函數(shù)參數(shù),這個(gè)函數(shù)參數(shù)是同步代碼,如下例子:
new Promise(() => {
console.log(1)
});
console.log(2)
打印結(jié)果為:
1
2
總結(jié)
本文簡(jiǎn)單介紹了 JavaScript 的并發(fā)模型,或者說 JavaScript 代碼的執(zhí)行順序。這里做一個(gè)總結(jié):
- JavaScript 主線程在運(yùn)行時(shí),如果發(fā)現(xiàn)異步方法,會(huì)將它們放入異步隊(duì)列中,同步方法則放入同步執(zhí)行棧中依次執(zhí)行
- 異步隊(duì)列中的代碼只有在同步執(zhí)行棧被清空后才有機(jī)會(huì)執(zhí)行
- 異步隊(duì)列又分為 Macro Tasks 隊(duì)列(宏任務(wù)隊(duì)列)和 Micro Tasks 隊(duì)列(微任務(wù)隊(duì)列),在執(zhí)行每一個(gè) Macro Task 之前,總是會(huì)先執(zhí)行 Micro Tasks 隊(duì)列中的代碼(若有),當(dāng) Micro Tasks 被清空之后,再去執(zhí)行 Macro Task。
附:參考資料
JavaScript并發(fā)模型與Event Loop
HTML系列:macrotask和microtask
MutationObserver
Navigator.sendBeacon
完。