下面就跟着這個大綱走,每一個點來講一下吧~html
JavaScript代碼的執行過程當中,除了依靠函數調用棧來搞定函數的執行順序外,還依靠任務隊列(task queue)來搞定另一些代碼的執行。整個執行過程,咱們稱爲事件循環過程。一個線程中,事件循環是惟一的,可是任務隊列能夠擁有多個。任務隊列又分爲macro-task(宏任務)與micro-task(微任務),在最新標準中,它們被分別稱爲task與jobs。前端
macro-task大概包括:html5
micro-task大概包括:node
總體執行,我畫了一個流程圖:git
總的結論就是,執行宏任務,而後執行該宏任務產生的微任務,若微任務在執行過程當中產生了新的微任務,則繼續執行微任務,微任務執行完畢後,再回到宏任務中進行下一輪循環。舉個例子: github
結合流程圖理解,答案輸出爲:async2 end => Promise => async1 end => promise1 => promise2 => setTimeout 可是,對於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
。async2 end
,此時將會保留async1函數的上下文,而後跳出async1函數。Promise
。遇到then,產生第一個微任務script end
promise1
,該微任務遇到then,產生一個新的微任務promise2
,當前微任務隊列執行完畢。執行權回到async1let promise_ = new Promise((resolve,reject){ resolve(undefined)})
複製代碼
執行完成,執行await後面的語句,輸出async1 end
瀏覽器
setTimeout
新版的chrome瀏覽器中不是如上打印的,由於chrome優化了,await變得更快了,輸出爲:
// script start => async2 end => Promise => script end => async1 end => promise1 => promise2 => setTimeout
複製代碼
可是這種作法實際上是違法了規範的,固然規範也是能夠更改的,這是 V8 團隊的一個 PR ,目前新版打印已經修改。 知乎上也有相關討論,能夠看看 www.zhihu.com/question/26…
咱們能夠分2種狀況來理解:
若是await 後面直接跟的爲一個變量,好比:await 1;這種狀況的話至關於直接把await後面的代碼註冊爲一個微任務,能夠簡單理解爲promise.then(await下面的代碼)。而後跳出async1函數,執行其餘代碼,當遇到promise函數的時候,會註冊promise.then()函數到微任務隊列,注意此時微任務隊列裏面已經存在await後面的微任務。因此這種狀況會先執行await後面的代碼(async1 end),再執行async1函數後面註冊的微任務代碼(promise1,promise2)。
若是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 處理非阻塞 I/O 操做的機制,node中事件循環的實現是依靠的libuv引擎。因爲 node 11 以後,事件循環的一些原理髮生了變化,這裏就以新的標準去講,最後再列上變化點讓你們瞭解來龍去脈。
node 中也有宏任務和微任務,與瀏覽器中的事件循環相似,其中,
macro-task 大概包括:
micro-task 大概包括:
先看一張官網的 node 事件循環簡化圖:
圖中的每一個框被稱爲事件循環機制的一個階段,每一個階段都有一個 FIFO 隊列來執行回調。雖然每一個階段都是特殊的,但一般狀況下,當事件循環進入給定的階段時,它將執行特定於該階段的任何操做,而後執行該階段隊列中的回調,直到隊列用盡或最大回調數已執行。當該隊列已用盡或達到回調限制,事件循環將移動到下一階段。
所以,從上面這個簡化圖中,咱們能夠分析出 node 的事件循環的階段順序爲:
輸入數據階段(incoming data)->輪詢階段(poll)->檢查階段(check)->關閉事件回調階段(close callback)->定時器檢測階段(timers)->I/O事件回調階段(I/O callbacks)->閒置階段(idle, prepare)->輪詢階段...
平常開發中的絕大部分異步任務都是在 poll、check、timers 這3個階段處理的,因此咱們來重點看看。
timers 階段會執行 setTimeout 和 setInterval 回調,而且是由 poll 階段控制的。 一樣,在 Node 中定時器指定的時間也不是準確時間,只能是儘快執行。
poll 是一個相當重要的階段,poll 階段的執行邏輯流程圖以下:
若是當前已經存在定時器,並且有定時器到時間了,拿出來執行,eventLoop 將回到 timers 階段。
若是沒有定時器, 會去看回調函數隊列。
若是 poll 隊列不爲空,會遍歷回調隊列並同步執行,直到隊列爲空或者達到系統限制
若是 poll 隊列爲空時,會有兩件事發生
check 階段。這是一個比較簡單的階段,直接執行 setImmdiate 的回調。
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'));
複製代碼
timeout1=>timeout2=>timeout3=>timeout4=>next tick1=>next tick2=>promise resolve
timeout1=>next tick1=>promise resolve=>timeout2=>next tick2=>timeout3=>timeout4
這裏主要說明的是 node11 先後的差別,由於 node11 以後一些特性已經向瀏覽器看齊了,總的變化一句話來講就是,若是是 node11 版本一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行對應的微任務隊列,一塊兒來看看吧~
setTimeout(()=>{
console.log('timer1')
Promise.resolve().then(function() {
console.log('promise1')
})
}, 0)
setTimeout(()=>{
console.log('timer2')
Promise.resolve().then(function() {
console.log('promise2')
})
}, 0)
複製代碼
timer1=>promise1=>timer2=>promise2
timer1=>promise1=>timer2=>promise2
timer1=>timer2=>promise1=>promise2
setImmediate(() => console.log('immediate1'));
setImmediate(() => {
console.log('immediate2')
Promise.resolve().then(() => console.log('promise resolve'))
});
setImmediate(() => console.log('immediate3'));
setImmediate(() => console.log('immediate4'));
複製代碼
immediate1=>immediate2=>promise resolve=>immediate3=>immediate4
immediate1=>immediate2=>immediate3=>immediate4=>promise resolve
setImmediate(() => console.log('timeout1'));
setImmediate(() => {
console.log('timeout2')
process.nextTick(() => console.log('next tick'))
});
setImmediate(() => console.log('timeout3'));
setImmediate(() => console.log('timeout4'));
複製代碼
timeout1=>timeout2=>next tick=>timeout3=>timeout4
timeout1=>timeout2=>timeout3=>timeout4=>next tick
以上幾個例子,你應該就能清晰感覺到它的變化了,反正記着一個結論,若是是 node11 版本一旦執行一個階段裏的一個宏任務(setTimeout,setInterval和setImmediate)就馬上執行對應的微任務隊列。
二者最主要的區別在於瀏覽器中的微任務是在每一個相應的宏任務中執行的,而nodejs中的微任務是在不一樣階段之間執行的。