面試題:說說事件循環機制(滿分答案來了)

答題大綱

  1. 先說基本知識點,宏任務、微任務有哪些
  2. 說事件循環機制過程,邊說邊畫圖出來
  3. 說async/await執行順序注意,能夠把 chrome 的優化,作法實際上是違法了規範的,V8 團隊的PR這些自信點說出來,顯得你很好學,理解得很詳細,很透徹。
  4. 把node的事件循環也說一下,重複一、二、3點,node中的第3點要說的是node11先後的事件循環變更點。

下面就跟着這個大綱走,每一個點來講一下吧~html

瀏覽器中的事件循環

JavaScript代碼的執行過程當中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列(task queue)來搞定另一些代碼的執行。整個執行過程,咱們稱爲事件循環過程。一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。前端

macro-task大概包括:html5

  • script(總體代碼)
  • setTimeout
  • setInterval
  • setImmediate
  • I/O
  • UI render

micro-task大概包括:node

  • process.nextTick
  • Promise
  • Async/Await(實際就是promise)
  • MutationObserver(html5新特性)

總體執行,我畫了一個流程圖:git

GitHub

總的結論就是,執行宏任務,而後執行該宏任務產生的微任務,若微任務在執行過程當中產生了新的微任務,則繼續執行微任務,微任務執行完畢後,再回到宏任務中進行下一輪循環。舉個例子: github

GitHub

結合流程圖理解,答案輸出爲:async2 end => Promise => async1 end => promise1 => promise2 => setTimeout 可是,對於async/await ,咱們有個細節還要處理一下。以下:面試

async/await執行順序

咱們知道async隱式返回 Promise 做爲結果的函數,那麼能夠簡單理解爲,await後面的函數執行完畢時,await會產生一個微任務(Promise.then是微任務)。可是咱們要注意這個微任務產生的時機,它是執行完await以後,直接跳出async函數,執行其餘代碼(此處就是協程的運做,A暫停執行,控制權交給B)。其餘代碼執行完畢後,再回到async函數去執行剩下的代碼,而後把await後面的代碼註冊到微任務隊列當中。咱們來看個例子:chrome

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')
 // 舊版輸出以下,可是請繼續看完本文下面的注意那裏,新版有改動
// script start => async2 end => Promise => script end => promise1 => promise2 => async1 end => setTimeout
複製代碼

分析這段代碼:promise

  • 執行代碼,輸出script start
  • 執行async1(),會調用async2(),而後輸出async2 end,此時將會保留async1函數的上下文,而後跳出async1函數。
  • 遇到setTimeout,產生一個宏任務
  • 執行Promise,輸出Promise。遇到then,產生第一個微任務
  • 繼續執行代碼,輸出script end
  • 代碼邏輯執行完畢(當前宏任務執行完畢),開始執行當前宏任務產生的微任務隊列,輸出promise1,該微任務遇到then,產生一個新的微任務
  • 執行產生的微任務,輸出promise2,當前微任務隊列執行完畢。執行權回到async1
  • 執行await,實際上會產生一個promise返回,即
let promise_ = new Promise((resolve,reject){ resolve(undefined)})
複製代碼

執行完成,執行await後面的語句,輸出async1 end瀏覽器

  • 最後,執行下一個宏任務,即執行setTimeout,輸出setTimeout

注意

新版的chrome瀏覽器中不是如上打印的,由於chrome優化了,await變得更快了,輸出爲:

// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
複製代碼

可是這種作法實際上是違法了規範的,固然規範也是能夠更改的,這是 V8 團隊的一個 PR ,目前新版打印已經修改。 知乎上也有相關討論,能夠看看 www.zhihu.com/question/26…

咱們能夠分2種狀況來理解:

  1. 若是await 後面直接跟的爲一個變量,好比:await 1;這種狀況的話至關於直接把await後面的代碼註冊爲一個微任務,能夠簡單理解爲promise.then(await下面的代碼)。而後跳出async1函數,執行其餘代碼,當遇到promise函數的時候,會註冊promise.then()函數到微任務隊列,注意此時微任務隊列裏面已經存在await後面的微任務。因此這種狀況會先執行await後面的代碼(async1 end),再執行async1函數後面註冊的微任務代碼(promise1,promise2)。

  2. 若是await後面跟的是一個異步函數的調用,好比上面的代碼,將代碼改爲這樣:

console.log('script start')

async function async1() {
    await async2()
    console.log('async1 end')
}
async function async2() {
    console.log('async2 end')
    return Promise.resolve().then(()=>{
        console.log('async2 end1')
    })
}
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')
複製代碼

輸出爲:

// script start => async2 end => Promise => script end => async2 end1 => promise1 => promise2 => async1 end => setTimeout
複製代碼

此時執行完awit並不先把await後面的代碼註冊到微任務隊列中去,而是執行完await以後,直接跳出async1函數,執行其餘代碼。而後遇到promise的時候,把promise.then註冊爲微任務。其餘代碼執行完畢後,須要回到async1函數去執行剩下的代碼,而後把await後面的代碼註冊到微任務隊列當中,注意此時微任務隊列中是有以前註冊的微任務的。因此這種狀況會先執行async1函數以外的微任務(promise1,promise2),而後才執行async1內註冊的微任務(async1 end). 能夠理解爲,這種狀況下,await 後面的代碼會在本輪循環的最後被執行. 瀏覽器中有事件循環,node 中也有,事件循環是 node 處理非阻塞 I/O 操做的機制,node中事件循環的實現是依靠的libuv引擎。因爲 node 11 以後,事件循環的一些原理髮生了變化,這裏就以新的標準去講,最後再列上變化點讓你們瞭解來龍去脈。

node 中的事件循環

瀏覽器中有事件循環,node 中也有,事件循環是 node 處理非阻塞 I/O 操做的機制,node中事件循環的實現是依靠的libuv引擎。因爲 node 11 以後,事件循環的一些原理髮生了變化,這裏就以新的標準去講,最後再列上變化點讓你們瞭解來龍去脈。

宏任務和微任務

node 中也有宏任務和微任務,與瀏覽器中的事件循環相似,其中,

macro-task 大概包括:

  • setTimeout
  • setInterval
  • setImmediate
  • script(總體代碼)
  • I/O 操做等。

micro-task 大概包括:

  • process.nextTick(與普通微任務有區別,在微任務隊列執行以前執行)
  • new Promise().then(回調)等。

node事件循環總體理解

先看一張官網的 node 事件循環簡化圖:

GitHub

圖中的每一個框被稱爲事件循環機制的一個階段,每一個階段都有一個 FIFO 隊列來執行回調。雖然每一個階段都是特殊的,但一般狀況下,當事件循環進入給定的階段時,它將執行特定於該階段的任何操做,而後執行該階段隊列中的回調,直到隊列用盡或最大回調數已執行。當該隊列已用盡或達到回調限制,事件循環將移動到下一階段。

所以,從上面這個簡化圖中,咱們能夠分析出 node 的事件循環的階段順序爲:

輸入數據階段(incoming data)->輪詢階段(poll)->檢查階段(check)->關閉事件回調階段(close callback)->定時器檢測階段(timers)->I/O事件回調階段(I/O callbacks)->閒置階段(idle, prepare)->輪詢階段...

階段概述

  • 定時器檢測階段(timers):本階段執行 timer 的回調,即 setTimeout、setInterval 裏面的回調函數。
  • I/O事件回調階段(I/O callbacks):執行延遲到下一個循環迭代的 I/O 回調,即上一輪循環中未被執行的一些I/O回調。
  • 閒置階段(idle, prepare):僅系統內部使用。
  • 輪詢階段(poll):檢索新的 I/O 事件;執行與 I/O 相關的回調(幾乎全部狀況下,除了關閉的回調函數,那些由計時器和 setImmediate() 調度的以外),其他狀況 node 將在適當的時候在此阻塞。
  • 檢查階段(check):setImmediate() 回調函數在這裏執行
  • 關閉事件回調階段(close callback):一些關閉的回調函數,如:socket.on('close', ...)。

三大重點階段

平常開發中的絕大部分異步任務都是在 poll、check、timers 這3個階段處理的,因此咱們來重點看看。

timers

timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。 一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。

poll

poll 是一個相當重要的階段,poll 階段的執行邏輯流程圖以下:

GitHub

若是當前已經存在定時器,並且有定時器到時間了,拿出來執行,eventLoop 將回到 timers 階段。

若是沒有定時器, 會去看回調函數隊列。

  • 若是 poll 隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或者達到系統限制

  • 若是 poll 隊列爲空時,會有兩件事發生

    • 若是有 setImmediate 回調須要執行,poll 階段會中止而且進入到 check 階段執行回調
    • 若是沒有 setImmediate 回調須要執行,會等待回調被加入到隊列中並當即執行回調,這裏一樣會有個超時時間設置防止一直等待下去,一段時間後自動進入 check 階段。
check

check 階段。這是一個比較簡單的階段,直接執行 setImmdiate 的回調。

process.nextTick

process.nextTick 是一個獨立於 eventLoop 的任務隊列。

在每個 eventLoop 階段完成後會去檢查 nextTick 隊列,若是裏面有任務,會讓這部分任務優先於微任務執行。

看一個例子:

setImmediate(() => {
    console.log('timeout1')
    Promise.resolve().then(() => console.log('promise resolve'))
    process.nextTick(() => console.log('next tick1'))
});
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick2'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
複製代碼
  • 在 node11 以前,由於每個 eventLoop 階段完成後會去檢查 nextTick 隊列,若是裏面有任務,會讓這部分任務優先於微任務執行,所以上述代碼是先進入 check 階段,執行全部 setImmediate,完成以後執行 nextTick 隊列,最後執行微任務隊列,所以輸出爲timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
  • 在 node11 以後,process.nextTick 是微任務的一種,所以上述代碼是先進入 check 階段,執行一個 setImmediate 宏任務,而後執行其微任務隊列,再執行下一個宏任務及其微任務,所以輸出爲timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4

node 版本差別說明

這裏主要說明的是 node11 先後的差別,由於 node11 以後一些特性已經向瀏覽器看齊了,總的變化一句話來講就是,若是是 node11 版本一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行對應的微任務隊列,一塊兒來看看吧~

timers 階段的執行時機變化

setTimeout(()=>{
    console.log('timer1')
    Promise.resolve().then(function() {
        console.log('promise1')
    })
}, 0)
setTimeout(()=>{
    console.log('timer2')
    Promise.resolve().then(function() {
        console.log('promise2')
    })
}, 0)
複製代碼
  • 若是是 node11 版本一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行微任務隊列,這就跟瀏覽器端運行一致,最後的結果爲timer1=>promise1=>timer2=>promise2
  • 若是是 node10 及其以前版本要看第一個定時器執行完,第二個定時器是否在完成隊列中.
    • 若是是第二個定時器還未在完成隊列中,最後的結果爲timer1=>promise1=>timer2=>promise2
    • 若是是第二個定時器已經在完成隊列中,則最後的結果爲timer1=>timer2=>promise1=>promise2

check 階段的執行時機變化

setImmediate(() => console.log('immediate1'));
setImmediate(() => {
    console.log('immediate2')
    Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
複製代碼
  • 若是是 node11 後的版本,會輸出immediate1=>immediate2=>promise resolve=>immediate3=>immediate4
  • 若是是 node11 前的版本,會輸出immediate1=>immediate2=>immediate3=>immediate4=>promise resolve

nextTick 隊列的執行時機變化

setImmediate(() => console.log('timeout1'));
setImmediate(() => {
    console.log('timeout2')
    process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
複製代碼
  • 若是是 node11 後的版本,會輸出timeout1=>timeout2=>next tick=>timeout3=>timeout4
  • 若是是 node11 前的版本,會輸出timeout1=>timeout2=>timeout3=>timeout4=>next tick

以上幾個例子,你應該就能清晰感覺到它的變化了,反正記着一個結論,若是是 node11 版本一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行對應的微任務隊列。

node 和 瀏覽器 eventLoop的主要區別

二者最主要的區別在於瀏覽器中的微任務是在每一個相應的宏任務中執行的,而nodejs中的微任務是在不一樣階段之間執行的。

更多理解資料

參考資料

最後

  • 歡迎加我微信(winty230),拉你進技術羣,長期交流學習...
  • 歡迎關注「前端Q」,認真學前端,作個有專業的技術人...

GitHub
相關文章
相關標籤/搜索