[基礎] 淺談 JS Event Loop

面試季臨近,Event Loop 這個概念也開始熱了,博客上處處都在寫,面試處處都在問,因而我也藉此機會查閱了一些相關資料彌補本身的知識盲區,把本身學習完以後對於瀏覽器的 Event Loop 寫一篇我的總結,有理解不對之處歡迎大佬指正~前端

這篇暫不作 Node 環境下的 Event Loop 的討論面試

JavaScript 裏的棧和隊列

在說 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

microtask 和 macrotask

若是隻想應付普通點的面試,上面一節的內容就足夠了,可是想要答出下面的這條面試題,就必須再次深刻 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),介於瀏覽器對兩種不一樣任務源隊列中回調函數的讀取機制,形成了上述代碼中的執行順序問題。

上圖摘自《掘金小冊:前端面試之道》

過程解析

讓咱們首先來分析一下上述代碼的執行流程:

  1. JavaScript 解析引擎在腳本開頭碰到了 console.log 因而打印 script strt
  2. 解析引擎解析至 async1()async1 執行環境被推入執行棧,解析引擎進入 async1 內部
  3. 引擎發現 async1 內部調用了 async2,因而繼續進入 async 2,並將 async 2 執行環境推入執行棧
  4. 引擎碰到 console.log,因而打印 async2 end
  5. async2 函數執行完成,返回了一個 Promise.resolve(undefined)此時,該回調被推入 microtask async1 函數中的執行權被讓出,等待主線程空閒
  6. 引擎解析至 setTimeout等待 0ms 後將其回調推入 macrotask,執行權繼續讓出
  7. 引擎指針繼續下移,直到碰到了 Promise,解析進入注入函數的內部,碰到 console.log,因而打印 Promise,再往下,碰到了 resolve此時,該回調被推入 microtask ,執行權被讓出
  8. 引擎繼續往下,碰到 console.log,打印完 script end
  9. 至此,主線程空閒,Event Loop 事件循環啓動,開始從 microtask 裏拿出 promise 回調,放入主線程執行,首先拿出最先注入的 async2Promise.resolve(undefined)執行,此時 await 操做符解析該表達式,獲得結果 undefined,並將 async1 [Promise] 函數 標誌爲 resolve 狀態,將 await 後面的代碼做爲回調,繼續推入 microtask,等待執行,執行權被讓出
  10. 此時主線程沒有可執行的代碼,再次空閒,Event Loop 啓動,去 microtask 中拿到以前的 new Promise 回調,放入主線程執行,打印結果 promise1promise2
  11. 主線程空閒,Event Loopmicrotask 裏拿 aysnc1 的回調,打印出 async1 end
  12. 最後,主線程空閒,microtask 隊列空,Event Loopmacrotask 裏拿到 setTimeout 的回調,放入主線程,打印最後的 setTimeout

常見的微任務和宏任務

微任務包括 process.nextTickpromiseMutationObserver,其中 process.nextTick 爲 Node 獨有。

宏任務包括 scriptsetTimeoutsetIntervalsetImmediateI/OUI rendering

關於 asyncawait

上述的面試題裏,大部分邏輯解釋下來都很好懂,除了一處,就是 await 後的 console.log(async1 end)new Promise resolve 後的回調,到底哪一個先執行?因爲瀏覽器底層的解析引擎實現不一樣,對於不一樣的瀏覽器其結果可能不同(最新版的 chrome 瀏覽器對於 await 的處理變快了,async1 會先於 promise 1 打印)。

可是相比於這個執行順序,上述題目衍生出的一個更重要的問題,是對於 async/await 的理解。

對於 async/await 的更詳細解釋,你們能夠參照這篇 理解 JavaScript 的 async/await,懶得看的童鞋能夠看下面的結論:

  1. 一個函數,只要被 async 關鍵字包裝過,就會返回一個 promise,若是該函數有返回值,那麼這個返回值就會做爲 then 處理的 response ,若是沒有返回值,那麼 then 就處理 undefined
  2. await 表達式,只能用在被 async 包裝過的函數裏,不然會報錯
  3. await 表達式後接的函數返回值,類型能夠爲 promise,或者其餘任何的值,await 後的代碼在當前執行環境下,會被阻塞至拿到該函數調用後的結果,等拿到結果後,會將 await 後面的代碼繼續包裝成新的 promise,並將以前拿到的結果做爲 response 傳入其中,同時讓出線程控權
  4. async/await 本質上是 Generator 的語法糖

宏任務與微任務,哪一個先執行?

關於這個問題,衆說紛紜,不少大佬都說是宏任務先於微任務執行,可是代碼的運行結果卻顯示是微任務先執行。 先看看大佬們的解釋:

這裏不少人會有個誤區,認爲微任務快於宏任務,實際上是錯誤的。由於宏任務中包括了 script ,瀏覽器會先執行一個宏任務,接下來有異步代碼的話纔會先執行微任務。 《掘金小冊:前端面試之道》

也就是說,Event Loop 抓取回調的邏輯是先執行宏任務,再執行微任務,再執行宏任務。。。以此循環,本質上來講,當前執行棧裏的代碼都屬於宏任務,因而等待執行棧清空,宏任務執行完成,瀏覽器回去 microtask 裏抓取微任務來執行,除非 microtask 裏沒有,纔會去 macrotask 抓取任務執行。

相關文章
相關標籤/搜索