先上一道常見的筆試題javascript
console.log('1'); async function async1() { console.log('2'); await async2(); console.log('3'); } async function async2() { console.log('4'); } process.nextTick(function() { console.log('5'); }) setTimeout(function() { console.log('6'); process.nextTick(function() { console.log('7'); }) new Promise(function(resolve) { console.log('8'); resolve(); }).then(function() { console.log('9') }) }) async1(); new Promise(function(resolve) { console.log('10'); resolve(); }).then(function() { console.log('11'); }); console.log('12');
你們能夠先配合下面這個圖片思考一下輸出順序及這麼運行的緣由
java
上面簡化圖解可拆分爲三部分:web
*Memory Heap 內存堆 —— 這是內存發生分配的地方面試
*Call Stack 調用棧 —— 這是代碼運行時棧幀存放的位置segmentfault
咱們要知道的是,像setTimeOut DOM AJAX,都不是由JavaScript引擎提供,而是由瀏覽器提供,統稱爲Web APIspromise
javascript是一門單線程語言,雖然HTML5提出了Web-works這樣的多線程解決方案,可是並無改變JaveScript是單線程的本質。瀏覽器
什麼是H5 Web Works?就是將一些大計算量的代碼交由web Worker運行而不凍結用戶界面,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質多線程
既然js是單線程的,就是同一時間只能作一件事情。那麼問題來了,咱們訪問一個頁面,這個頁面的初始化代碼運行時間很長,好比有不少圖片、視頻、外部資源等等,難道咱們也要一直在那等着嗎?答案固然是 不能異步
因此就出現了兩類任務:async
那麼咱們怎麼知道何時主線程是空的呢?js引擎存在monitoring process進程,會持續不斷的檢查主線程執行棧是否爲空,一旦爲空,就會去Event Queue那裏檢查是否有等待被調用的函數。
setTimeout(fn,0)
這裏的延遲0秒時什麼意思呢?
含義是,當主線程執行棧內爲空時,不用等待,就立刻執行。
setInterval和setTimeout相似,只是前者是循環的執行。對於執行順序來講,setInterval
會每隔指定的時間將註冊的函數置入Event Queue,若是前面的任務耗時過久,那麼一樣須要等待。
對於setInterval(fn,ms)
來講,咱們已經知道不是每過ms
秒會執行一次fn
,而是每過ms
秒,會有fn
進入Event Queue。一旦setInterval的回調函數fn執行時間超過了延遲時間ms,那麼就徹底看不出來有時間間隔了。
事件循環的順序,決定js代碼的執行順序。進入總體代碼(宏任務)後,開始第一次循環。接着執行全部的微任務。而後再次從宏任務開始,找到其中一個任務隊列執行完畢,再執行全部的微任務。
除了廣義的同步任務和異步任務,咱們對任務有更精細的定義:
setTimeout(fn, 0)在下一輪「事件循環」開始時執行,Promise.then()在本輪「事件循環」結束時執行。
不一樣類型的任務會進入對應的Event Queue:
Promise中的異步體如今then
和catch
中,因此寫在Promise中的代碼是被當作同步任務當即執行的。
await其實是一個讓出線程的標誌。await後面的表達式會先執行一遍,將await後面的代碼加入到microtask中,而後就會跳出整個async函數來執行後面的代碼;
由於async await 自己就是promise+generator的語法糖。因此await後面的代碼是microtask。
下面開始分析開頭的代碼
console.log('1'); async function async1() { console.log('2'); await async2(); console.log('3'); } async function async2() { console.log('4'); } process.nextTick(function() { console.log('5'); }) setTimeout(function() { console.log('6'); process.nextTick(function() { console.log('7'); }) new Promise(function(resolve) { console.log('8'); resolve(); }).then(function() { console.log('9') }) }) async1(); new Promise(function(resolve) { console.log('10'); resolve(); }).then(function() { console.log('11'); }); console.log('12');
第一輪事件循環流程:
script
做爲第一個宏任務進入主線程,遇到console.log
,輸出1async一、async2
函數聲明,聲明暫時不用管process.nextTick()
,其回調函數被分發到微任務Event Queue中。咱們記爲process1
setTimeout
,其回調函數被分發到宏任務Event Queue中。咱們暫且記爲setTimeout1
async1
,遇到console.log
,輸出2下面這裏是最難理解的地方
咱們知道使用 async 定義的函數,當它被調用時,它返回的是一個Promise對象
而當await後面的表達式是一個Promise時,它的返回值其實是Promise的回調函數resolve的參數
await async2()
調用,發現async2也是一個 async 定義的函數,全部直接執行輸出4,同時返回了一個Promise。劃重點:此時返回的Promise被分配到微任務Event Queue中,咱們記爲await1。await會讓出線程,接下來就會跳出async1函數繼續往下執行。 Promise
,new Promise
直接執行,輸出10。then
被分發到微任務Event Queue中。咱們記爲then1
console.log
,輸出12宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout1 | process1 |
await1 | |
then1 |
上表是第一輪事件循環宏任務結束時各Event Queue的狀況,此時已經輸出了1 2 4 10 12
咱們發現了process1
、await1
和then1
三個微任務
process1
,輸出5await1
,就是 async1 放進去的Promise,執行Promise時發現又遇到了他的真命天子resolve函數,劃重點:這個resolve又會被放入微任務Event Queue中,咱們記爲await2,而後再次跳出 async1函數 繼續下一個任務。 then1
,輸出11宏任務Event Queue | 微任務Event Queue |
---|---|
setTimeout1 | await2 |
到這裏,已經輸出了1 2 4 10 12 5 11
此時還有一個await2
微任務
它是async1 放進去的Promise的resolve回調,執行它(由於 async2 並無return東西,因此這個resolve的參數是undefined),此時 await 定義的這個 Promise 已經執行完而且返回告終果,因此能夠繼續往下執行 async1函數 後面的任務了,那就是console.log(3)
,輸出3
到這裏,第一輪事件循環結束,此時,輸出順序是 1 2 4 10 12 5 11 3
第二輪時間循環從setTimeout1
宏任務開始
console.log
,輸出6process.nextTick()
,一樣將其分發到微任務Event Queue中,記爲process2
new Promise
當即執行輸出8,then
也分發到微任務Event Queue中,記爲then2
宏任務Event Queue | 微任務Event Queue |
---|---|
process2 | |
then2 |
上表是第二輪事件循環宏任務結束時各Event Queue的狀況,此時輸出狀況是
咱們發現了process2
和then2
兩個微任務
process2
,輸出7then2
,輸出9第二輪事件循環結束,第二輪輸出6 8 7 9
整段代碼,共進行了兩次事件循環,完整的輸出 1 2 4 10 12 5 11 3 6 8 7 9
到這裏,你們應該已經清楚了JS的事件循環機制,後面無論在工做仍是面試中,確定都是遊刃有餘啦~
本篇是我開始的第一篇文章,還但願你們多多支持,不吝賜教哇,也但願能夠提出意見或建議。
資料參考: