大概是大半年前,我在微信上看到了一篇前端文章。文章開頭就拿了一道面試題,讓讀者寫出答案。題目乍看上去不難,都是console.log()的代碼,惟一讓我以爲有貓膩的是多了一些setTimeout和Promise。我也沒想太多,信心滿滿地就開始作題了。果不其然,和正確答案南轅北轍。看做者後續的解釋,才第一次接觸到了宏任務與微任務這兩個概念。當時理解了一點,知道這兩個概念都是用於區分異步任務的。隨後就過了大半年,直到前幾天又看了一道題。答案差了一半,腦海裏對於宏任務和微任務的理解又變得模糊了。反思了一下,仍是當初對於知識的理解就不深入,沒有化爲本身的東西。這也就是基礎不紮實的一個表現吧。javascript
在掘金上看了很多同行的文章,但都不是本身的理解,在此總結一下本身的理解。前端
在說宏任務和微任務以前,先說一下一些基礎的概念。JavaScript是1995年誕生的,最初的目的是賦予網頁生命,在網頁上執行一些邏輯,增長用戶與網頁的交互,讓網頁變得更加有「智能」。由於最初的目的很簡單,因此註定了JS並不複雜,也不容許很複雜。在這個基礎上,JS的一個特質就凸顯出來了,那就是單線程。java
單線程是什麼意思呢?意思就是JS引擎一次只能處理一件事情,若是有多件事情須要處理,那麼JS引擎會一件一件的來。咱們能夠認爲JS引擎就像郭靖同樣,憨憨的,學不會周伯通的左右互搏。既然有單線程語言,那確定也有多線程語言了。沒錯,基本上後端語言都是多線程的,就像黃蓉同樣,一心多用,古靈精怪。面試
這個世界是客觀的,獲得了一些東西相應地也會失去一些東西。箇中好壞都憑本身去判斷與接受。單線程的JS帶來的好處就是簡單,不會出現多個線程同時修改DOM的狀況,那樣網頁將會變得一團糟。ajax
其實,JavaScript 單線程指的是瀏覽器中負責解釋和執行 JavaScript 代碼的只有一個線程,即爲JS引擎線程,可是瀏覽器的渲染進程是提供多個線程的,以下:編程
這裏講一下同步和異步,這兩個概念是脫離編程語言的。映射的是現實世界中的狀況。同步就是立刻就能完成的事情,異步就是一時半會不能完成的事情。後端
舉個例子,打開課本,這就是一個同步事件。你立刻就能作到,沒有絲毫的延遲和不肯定性。再看水壺燒水,這是一個異步事件。由於你不能控制水燒開,只能等待水被燒開。對比了一下,異步事件比同步事件多了「不肯定性」,或者說引入了「變量」。反映到代碼上,下面的第一行代碼就是同步代碼,直接打印出1。第二行代碼定時器 setTimeout,是一個異步事件,由於須要等待一個設置好的時間200ms才能完成打印。即便這個時間是0,也會被JS引擎納入異步事件。固然了,ajax請求更是典型的異步事件,須要等待服務器的響應。瀏覽器
console.log(1);
setTimeout(() => {
console.log(2);
},200);
複製代碼
異步事件中有一個回調函數的概念,由於異步事件不是一時半會就能完成了。那麼當異步事件完成以後,須要作的事情是什麼呢?比如上面說的水壺燒水,水開了以後,我要喝水,這就是一個回調邏輯。也能夠等水開了,我要洗衣服,這也是一個回調邏輯。因此說異步事件,須要註冊一個回調函數,表示異步事件完成以後須要作的處理。那麼這裏隱藏了一個問題,那就是誰在異步事件完成以後通知JS引擎呢?答案是瀏覽器的線程,負責監聽異步事件的完成。bash
最開始說了JS是單線程語言,一次只能作一件事,那麼碰到同步事件就很簡單了,不廢話,直接作。可是遇到異步事件,就有點複雜了,JS引擎須要考慮如下幾個問題:服務器
爲了解決異步事件的執行順序問題,JS引擎產生了一個機制,那就是Event Loop(事件循環)。經過這個機制,JS引擎對異步事件進行處理,一次處理一件。
首先咱們要明確的是在最高層級,同步事件先於異步事件執行。那麼如何執行多個異步事件,就須要「事件循環」的機制來處理多個異步事件的前後順序。
解讀:
咱們不由要問了,那怎麼知道主線程執行棧爲空呢?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。
如上面的流程圖所示,異步任務會首先進入Event Table並註冊了回調函數,當指定的事情完成時,Event Table會將這個函數移入Event Queue。問題來了,不一樣的異步事件,會進入不一樣的Event Queue。這裏就進一步把異步事件劃分爲宏任務和微任務。
舉個例子,去銀行辦理業務,首先須要取號等到叫到本身。通常上邊都會印着相似:「您的號碼爲XX,前邊還有XX人。」之類的字樣。由於一個窗口的櫃員同時只能處理一個來辦理業務的客戶,對於櫃員來講,每個來辦理業務的客戶都是一個宏任務。當櫃員處理完當前客戶的問題之後,選擇接待下一位,廣播保號,也就是下一個宏任務的開始。類比Excel中的「宏」的概念,這裏的「宏」有着總體的概念。
因此多個宏任務合在一塊兒就能夠說一個任務隊列在這,隊列裏是當前銀行中全部排號的客戶。任務隊列中的都是已經完成的異步操做,而不是說註冊一個異步任務就會被放在這個任務隊列中,就像在銀行中排號,若是叫到你的時候你不在,那麼你當前的號牌就做廢了,櫃員會選擇直接跳過進行下一個客戶的業務處理,等你回來之後還須要從新取號。 並且一個宏任務在執行的過程當中,是能夠添加一些微任務的,就像在櫃檯辦理業務,你前面的一位老大爺可能在存款,在存款這個業務辦理完以後,櫃員詢問大爺還有沒有其餘要辦的業務。大爺說像辦理理財,那麼櫃員會繼續爲大爺辦理理財業務,而不會直接辦理下一位客戶的業務。因此原本快輪到你來辦理業務,會由於老大爺臨時添加的「理財業務」而日後推。也許老大爺在辦完理財之後還想 再辦一個信用卡?或者 再買點兒記念幣?不管是什麼需求,只要是櫃員可以幫她辦理的,都會在處理你的業務以前來作這些事情,這些均可以認爲是微任務。
這裏就說明了**同一批次的微任務上下文是相同的,依賴於當前的宏任務。**因此當一個宏任務完成後,主線程會詢問有沒有微任務須要處理,只能處理完了當前全部的微任務,纔會開始下一個宏任務。
下面看一下我以前弄錯了的題目:
console.log(1)
setTimeout(()=>{console.log(2)},1000)
async function fn(){
console.log(3)
setTimeout(()=>{console.log(4)},20)
return Promise.reject()
}
async function run(){
console.log(5)
await fn()
console.log(6)
}
run()
//須要執行150ms左右
for(let i=0;i<90000000;i++){}
setTimeout(()=>{
console.log(7)
new Promise(resolve=>{
console.log(8)
resolve()
}).then(()=>{console.log(9)})
},0)
console.log(10)
// 1 5 3 10 4 7 8 9 2
複製代碼
在作這道題以前須要明確幾個概念:
new Promise
在實例化的過程當中所執行的代碼都是同步進行的,而then
中註冊的回調纔是異步執行的。接下來咱們一步一步分析:
第一行代碼console.log(1)
是同步任務,直接打印出1
。
第二行代碼碰到了setTimeout,異步任務而且屬於宏任務。首先將setTimeout放進Event Table,並註冊回調函數,在1000ms以後,再將回調函數放入宏任務隊列。因此此時宏任務隊列暫時爲空
Event Table | 宏任務隊列 | 微任務隊列 |
---|---|---|
setTimeout1(1000ms) | 空 | 空 |
接下來是執行run(),雖然函數run,前使用了async關鍵詞,表示內部有異步事件。可是不影響函數其餘同步代碼執行。代碼console.log(5)
執行,打印5
。而後碰到了關鍵詞await
,繼續執行函數fn,代碼console.log('3')
執行,打印3
。這時又碰到了setTiemout,將其放入Event Table中。再看,setTimeout後面的Promise.reject屬於微任務,將其放入微任務隊列。
Event Table | 宏任務隊列 | 微任務隊列 |
---|---|---|
setTimeout1(1000ms) | 空 | Promise.reject() |
setTimeout2(20ms) | 空 | 空 |
這裏須要注意的是:因爲函數fn沒有執行完成,awit fn()後面的代碼是不會執行的,瀏覽器會繼續執行後面的for循環。
而後執行for循環,因爲for循環屬於同步任務,這個時候主線程的工做就是執行for循環邏輯,即便循環裏面爲空,也須要花費150ms。當150ms以後,for循環執行完成,此時Event Table裏面的20ms的setTimeout也已經到了時間,回調函數開始被放入宏任務隊列了。
Event Table | 宏任務對列 | 微任務隊列 |
---|---|---|
setTimeout1(1000ms) | setTimeout2(20ms) | Promise.reject() |
空 | 空 | 空 |
主線程繼續執行代碼,又又又碰到了setTimeout,這個時候依舊放入Event Table中。
接着執行代碼console.log(10)
,打印10
。本次宏任務結束,本次大的腳本視爲一次宏任務。
宏任務結束後,主線程詢問是否有微任務須要執行,此時微任務中存在Promise.reject()
,執行這個任務。而後函數run中的await fn()
完成,注意由於await右邊的Promise返回的是reject,因此後面的代碼都不會執行。微任務執行完成。
Event Table | 宏任務隊列 | 微任務隊列 |
---|---|---|
setTimeout1(1000ms) | setTimeout2(20ms) | 空 |
空 | setTimeout3(0ms) | 空 |
下面開始下一次的宏任務,執行setTimeout2的回調函數,請注意進入任務隊列的都是已經完成的異步事件的回調函數。setTimeout2的回調函數開始執行,console.log(4)
打印出4
。
Event Table | 宏任務隊列 | 微任務隊列 |
---|---|---|
setTimeout1(1000ms) | setTimeout3(0ms) | 空 |
空 | 空 | 空 |
下面繼續執行下一個宏任務setTimeout3的回調函數。console.log(7)
打印7
。而後碰到new Promise
,執行同步代碼console.log(8)
打印8
。將then
裏註冊的回調函數放入微任務隊列。
Event Table | 宏任務隊列 | 微任務隊列 |
---|---|---|
setTimeout1(1000ms) | 空 | then |
本次宏任務執行結束,開始執行微任務,console.log(9)
執行,打印9
。
此時若是時間尚未到1000ms,那麼須要等待時間完成後,將setTimeout1放入宏任務隊列中。
Event Table | 宏任務隊列 | 微任務隊列 |
---|---|---|
空 | setTimeout1(1000ms) | 空 |
最後執行宏任務setTimeout1,console.log(2)
執行,打印2
。
到此,代碼所有執行完畢。