以前寫了一篇 Event Loop 的文章,然而在後續的深刻學習中發現當時有些概念的理解是錯誤的,因而又從新修改了一下,更新上來。javascript
javascript
是單線程語言,使用的是異步非阻塞的運行方式,不少狀況下須要經過事件和回調函數進行驅動,那麼這些註冊的回調函數,是在何時被運行環境調用的,彼此之間又是以怎樣的順序執行的?這就繞不開一個機制——Event Loop ,也就是事件循環。前端
JS Event Loop 即事件循環,是運行在瀏覽器環境 / Node 環境中的一種消息通訊機制,它是主線程以外的獨立線程。當主線程內須要執行某些可能致使線程阻塞的耗時操做時(好比請求發送與接收響應、文件 I/O、數據計算)主線程會註冊一個回調函數並拋給 Event Loop 線程進行監聽,本身則繼續往下執行,一旦有消息返回而且主線程空閒的狀況下,Event Loop 會及時通知主線程,執行對應的回調函數獲取信息,以此達到非阻塞的目的。java
在解析 Event Loop
運行機制以前,咱們要先理解棧(stack
)和隊列(queue
)的概念。 棧和隊列,二者都是線性結構,可是棧遵循的是後進先出(last in first off LIFO),開口封底。而隊列遵循的是先進先出 (fisrt in first out,FIFO),兩頭通透。webpack
Event Loop
得以順利執行,它所依賴的容器環境,就和這兩個概念有關。git
咱們知道,在 js
代碼執行過程當中,會生成一個當前環境的「執行上下文( 執行環境 / 做用域)」,用於存放當前環境中的變量,這個上下文環境被生成之後,就會被推入js
的執行棧。一旦執行完成,那麼這個執行上下文就會被執行棧彈出,裏面相關的變量會被銷燬,在下一輪垃圾收集到來的時候,環境裏的變量佔據的內存就能得以釋放。web
這個執行棧,也能夠理解爲JavaScript
的單一線程,全部代碼都跑在這個裏面,以同步的方式依次執行,或者阻塞,這就是同步場景。面試
那麼異步場景呢?顯然就須要一個獨立於「執行棧」以外的容器,專門管理這些異步的狀態,因而在「主線程」、「執行棧」以外,有了一個 Task
的隊列結構,專門用於管理異步邏輯。全部異步操做的回調,都會暫時被塞入這個隊列。Event Loop
處在二者之間,扮演一個大管家的角色,它會以一個固定的時間間隔不斷輪詢,當它發現主線程空閒,就會去到 Task
隊列裏拿一個異步回調,把它塞入執行棧中執行,一段時間後,主線程執行完成,彈出上下文環境,再次空閒,Event Loop
又會執行一樣的操做。。。依次循環,因而構成了一套完整的事件循環運行機制。express
上圖比較簡潔地描繪了整個過程,只不過其中多了
heap
(堆)的概念,堆和棧,簡單來講,堆是留給開發者分配的內存空間,而棧是原生編譯器要使用的內存空間,兩者獨立。promise
若是隻想應付普通點的面試,上面一節的內容就足夠了,可是想要答出下面的這條面試題,就必須再次深刻 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 -> async1 end -> promise1 -> promise2 -> setTimeout 複製代碼
若是隻有一個單一的 Task
隊列,就不存在上面的順序問題了。但事實狀況是,瀏覽器會根據任務性質的不一樣,將不一樣的任務源塞入不一樣的隊列中,任務源能夠分爲微任務(microtask
) 和宏任務(macrotask
),介於瀏覽器對兩種不一樣任務源隊列中回調函數的讀取機制,形成了上述代碼中的執行順序問題。
微任務包括 process.nextTick
,promise
,MutationObserver
,其中 process.nextTick
爲 Node 獨有。
宏任務包括 script
, setTimeout
,setInterval
,setImmediate
,I/O
,UI rendering
。
瞭解了微任務宏任務的概念後,咱們就能夠完整地分析一邊 Event Loop
的執行機制了。
micro-task
爲空,macro-task
裏有且只有一個 script
腳本(總體代碼)script
腳本執行:全局上下文(script 任務)被推入執行棧,代碼以同步的方式以此執行。在執行的過程當中,可能會產生新的 macro-task
與 micro-task
,它們會分別被推入各自的任務隊列裏script
腳本出隊:同步代碼執行完了,script
腳本會被移出 macro-task
,這個過程本質上是宏任務隊列的執行和出隊的過程。script
宏任務執行並出隊了,這時候執行棧爲空,Event Loop
會去 micro-task
中將微任務推入主線程執行,這裏的微任務的執行方式和宏任務的執行方式有個很重要的區別,就是:宏任務是一個一個執行,而微任務是一隊一隊執行的。也就是說,執行一個宏任務,要執行一隊的微任務。(注意:在執行微任務的過程當中,仍有可能有新的微任務插入 micro-task
那麼這種狀況下,Event Loop
仍然須要將本次 Tick
(循環) 下的微任務拿到主線程中執行完畢)Web worker
任務,若是有,則對其進行處理 。在上面瀏覽器 Event Loop 的執行機制中,有很重要的一塊內容,就是瀏覽器的渲染時機,瀏覽器會等到當前的 micro-task
爲空的時候,進行一次從新渲染。因此若是你須要在異步的操做後從新渲染 DOM 最好的方法是將它包裝成 micro
任務,這樣 DOM 渲染將會在本次 Tick
內就完成。
看到這裏,相信你已經明白上面的那條面試題是怎麼一回事了,咱們能夠用對 Event Loop 的理解來分析一下這道題目的執行:
// 請給出下面這段代碼執行後,log 的打印順序 console.log('script start') // 這邊的 await 可能不太好理解,我換成了另外一種寫法 function async1() { async2().then(res => { console.log('async1 end') }) } function async2() { console.log('async2 end') return Promise.resolve(undefined); } 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 -> async1 end -> promise1 -> promise2 -> setTimeout 複製代碼
本篇文章已收錄入 前端面試指南專欄