面試季臨近,Event Loop
這個概念也開始熱了,博客上處處都在寫,面試處處都在問,因而我也藉此機會查閱了一些相關資料彌補本身的知識盲區,把本身學習完以後對於瀏覽器的 Event Loop
寫一篇我的總結,有理解不對之處歡迎大佬指正~前端
這篇暫不作
Node
環境下的Event Loop
的討論面試
在說 Event Loop
以前,咱們要先理解棧(stack
)和隊列(queue
)的概念。chrome
棧和隊列,二者都是線性結構,可是棧遵循的是後進先出(last in first off,LIFO
),開口封底。而隊列遵循的是先進先出 (fisrt in first out,FIFO
),兩頭通透。segmentfault
Event Loop
得以順利執行,它所依賴的容器環境,就和這兩個概念有關。promise
咱們知道,在 js
代碼執行過程當中,會生成一個當前環境的「執行上下文( 執行環境 / 做用域)」,用於存放當前環境中的變量,這個上下文環境被生成之後,就會被推入js
的執行棧。一旦執行完成,那麼這個執行上下文就會被執行棧彈出,裏面相關的變量會被銷燬,在下一輪垃圾收集到來的時候,環境裏的變量佔據的內存就能得以釋放。瀏覽器
這個執行棧,也能夠理解爲JavaScript
的單一線程,全部代碼都跑在這個裏面,以同步的方式依次執行,或者阻塞,這就是同步場景。bash
那麼異步場景呢?顯然就須要一個獨立於「執行棧」以外的容器,專門管理這些異步的狀態,因而在「主線程」、「執行棧」以外,有了一個 Task
的隊列結構,專門用於管理異步邏輯。全部異步操做的回調,都會暫時被塞入這個隊列。Event Loop
處在二者之間,扮演一個大管家的角色,它會以一個固定的時間間隔不斷輪詢,當它發現主線程空閒,就會去到 Task
隊列裏拿一個異步回調,把它塞入執行棧中執行,一段時間後,主線程執行完成,彈出上下文環境,再次空閒,Event Loop
又會執行一樣的操做。。。依次循環,因而構成了一套完整的事件循環運行機制。異步
上圖是筆者在 Google 上找的,比較簡潔地描繪了整個過程,只不過其中多了
heap
(堆)的概念,堆和棧,簡單來講,堆是留給開發者分配的內存空間,而棧是原生編譯器要使用的內存空間,兩者獨立。async
若是隻想應付普通點的面試,上面一節的內容就足夠了,可是想要答出下面的這條面試題,就必須再次深刻 Event Loop
,瞭解任務隊列的深層原理:microtask
(微任務)和 macrotask
(宏任務)。函數
// 請給出下面這段代碼執行後,log 的打印順序
console.log('script start')
async function async1() {
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2 end')
}
async1()
setTimeout(function() {
console.log('setTimeout')
}, 0)
new Promise(resolve => {
console.log('Promise')
resolve()
})
.then(function() {
console.log('promise1')
})
.then(function() {
console.log('promise2')
})
console.log('script end')
// log 打印順序:script start -> async2 end -> Promise -> script end -> promise1 -> promise2 -> async1 end -> setTimeout
複製代碼
若是隻有一個單一的 Task
隊列,就不存在上面的順序問題了。但事實狀況是,瀏覽器會根據任務性質的不一樣,將不一樣的任務源塞入不一樣的隊列中,任務源能夠分爲微任務(microtask
) 和宏任務(macrotask
),介於瀏覽器對兩種不一樣任務源隊列中回調函數的讀取機制,形成了上述代碼中的執行順序問題。
上圖摘自《掘金小冊:前端面試之道》
讓咱們首先來分析一下上述代碼的執行流程:
JavaScript
解析引擎在腳本開頭碰到了 console.log
因而打印 script strt
async1()
,async1
執行環境被推入執行棧,解析引擎進入 async1
內部async1
內部調用了 async2
,因而繼續進入 async 2
,並將 async 2
執行環境推入執行棧console.log
,因而打印 async2 end
async2
函數執行完成,返回了一個 Promise.resolve(undefined)
,此時,該回調被推入 microtask ,async1
函數中的執行權被讓出,等待主線程空閒setTimeout
,等待 0ms 後將其回調推入 macrotask,執行權繼續讓出Promise
,解析進入注入函數的內部,碰到 console.log
,因而打印 Promise
,再往下,碰到了 resolve
,此時,該回調被推入 microtask ,執行權被讓出console.log
,打印完 script end
async2
的 Promise.resolve(undefined)
執行,此時 await 操做符解析該表達式,獲得結果 undefined,並將 async1 [Promise] 函數 標誌爲 resolve 狀態,將 await 後面的代碼做爲回調,繼續推入 microtask,等待執行,執行權被讓出new Promise
回調,放入主線程執行,打印結果 promise1
和 promise2
Event Loop
去 microtask
裏拿 aysnc1
的回調,打印出 async1 end
microtask
隊列空,Event Loop
去 macrotask
裏拿到 setTimeout
的回調,放入主線程,打印最後的 setTimeout
微任務包括 process.nextTick
,promise
,MutationObserver
,其中 process.nextTick
爲 Node 獨有。
宏任務包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
async
和 await
上述的面試題裏,大部分邏輯解釋下來都很好懂,除了一處,就是 await
後的 console.log(async1 end)
和 new Promise
resolve
後的回調,到底哪一個先執行?因爲瀏覽器底層的解析引擎實現不一樣,對於不一樣的瀏覽器其結果可能不同(最新版的 chrome 瀏覽器對於 await 的處理變快了,async1 會先於 promise 1 打印)。
可是相比於這個執行順序,上述題目衍生出的一個更重要的問題,是對於 async/await
的理解。
對於 async/await
的更詳細解釋,你們能夠參照這篇 理解 JavaScript 的 async/await,懶得看的童鞋能夠看下面的結論:
async
關鍵字包裝過,就會返回一個 promise
,若是該函數有返回值,那麼這個返回值就會做爲 then
處理的 response
,若是沒有返回值,那麼 then
就處理 undefined
await
表達式,只能用在被 async
包裝過的函數裏,不然會報錯await
表達式後接的函數返回值,類型能夠爲 promise
,或者其餘任何的值,await
後的代碼在當前執行環境下,會被阻塞至拿到該函數調用後的結果,等拿到結果後,會將 await
後面的代碼繼續包裝成新的 promise
,並將以前拿到的結果做爲 response
傳入其中,同時讓出線程控權async/await
本質上是 Generator
的語法糖關於這個問題,衆說紛紜,不少大佬都說是宏任務先於微任務執行,可是代碼的運行結果卻顯示是微任務先執行。 先看看大佬們的解釋:
這裏不少人會有個誤區,認爲微任務快於宏任務,實際上是錯誤的。由於宏任務中包括了
script
,瀏覽器會先執行一個宏任務,接下來有異步代碼的話纔會先執行微任務。 《掘金小冊:前端面試之道》
也就是說,Event Loop
抓取回調的邏輯是先執行宏任務,再執行微任務,再執行宏任務。。。以此循環,本質上來講,當前執行棧裏的代碼都屬於宏任務,因而等待執行棧清空,宏任務執行完成,瀏覽器回去 microtask
裏抓取微任務來執行,除非 microtask
裏沒有,纔會去 macrotask
抓取任務執行。