JavaScript 的并發(fā)模型

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


JavaScript主線程的棧和隊(duì)列.png

異步代碼的執(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)系如圖所示:


異步隊(duì)列.png

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é):

  1. JavaScript 主線程在運(yùn)行時(shí),如果發(fā)現(xiàn)異步方法,會(huì)將它們放入異步隊(duì)列中,同步方法則放入同步執(zhí)行棧中依次執(zhí)行
  2. 異步隊(duì)列中的代碼只有在同步執(zhí)行棧被清空后才有機(jī)會(huì)執(zhí)行
  3. 異步隊(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

完。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容