背景
一天愜意的下午。朋友給我分享了一道頭條面試題,以下:html
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
複製代碼複製代碼
這個題目主要是考察對同步任務、異步任務:setTimeout、promise、async/await的執行順序的理解程度。(建議你們也本身先作一下o)node
當時因爲我對async、await瞭解的不是很清楚,答案錯的千奇百怪 :(),就不記錄了,而後我就去看文章理了理思路。如今寫在下面以供往後參考。面試
js事件輪詢的一些概念
這裏首先須要明白幾個概念:同步任務、異步任務、任務隊列、microtask、macrotaskchrome
同步任務 指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;promise
異步任務 指的是,不進入主線程、而進入"任務隊列"(task queue)的任務,等待同步任務執行完畢以後,輪詢執行異步任務隊列中的任務數據結構
macrotask隊列 等同於咱們常說的任務隊列,macrotask是由宿主環境分發的異步任務,事件輪詢的時候老是一個一個任務隊列去查看執行的,"任務隊列"是一個先進先出的數據結構,排在前面的事件,優先被主線程讀取。異步
microtask 是由js引擎分發的任務,老是添加到當前任務隊列末尾執行。另外在處理microtask期間,若是有新添加的microtasks,也會被添加到隊列的末尾並執行。注意與setTimeout(fn,0)的區別:async
setTimeOut(fn(),0) 指定某個任務在主線程最先可得的空閒時間執行,也就是說,儘量早得執行。它在"任務隊列"的尾部添加一個事件,所以要等到同步任務和"任務隊列"現有的事件都處理完,纔會獲得執行。函數
總結一下:oop
task queue、microtask、macrotask
- An event loop has one or more task queues.(task queue is macrotask queue)
- Each event loop has a microtask queue.
- task queue = macrotask queue != microtask queue
- a task may be pushed into macrotask queue,or microtask queue
- when a task is pushed into a queue(micro/macro),we mean preparing work is finished,so the task can be executed now.
因此咱們能夠獲得js執行順序是:
開始 -> 取第一個task queue裏的任務執行(能夠認爲同步任務隊列是第一個task queue) -> 取microtask所有任務依次執行 -> 取下一個task queue裏的任務執行 -> 再次取出microtask所有任務執行 -> … 這樣循環往復
常見的一些宏任務和微任務:
macrotask:
- setTimeout
- setInterval
- setImmediate
- requestAnimationFrame
- I/O
- UI rendering
microtask:
- process.nextTick
- Promises
- Object.observe
- MutationObserver
Promise、Async、Await都是一種異步解決方案
Promise是一個構造函數,調用的時候會生成Promise實例。當Promise的狀態改變時會調用then函數中定義的回調函數。咱們都知道這個回調函數不會馬上執行,他是一個微任務會被添加到當前任務隊列中的末尾,在下一輪任務開始執行以前執行。
async/await成對出現,async標記的函數會返回一個Promise對象,可使用then方法添加回調函數。await後面的語句會同步執行。但 await 下面的語句會被當成微任務添加到當前任務隊列的末尾異步執行。
咱們來看一下答案
不記得題的!繼續往下看,舒適的準備了題目,不用往上翻
async function async1(){
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2(){
console.log('async2')
}
console.log('script start')
setTimeout(function(){
console.log('setTimeout')
},0)
async1();
new Promise(function(resolve){
console.log('promise1')
resolve();
}).then(function(){
console.log('promise2')
})
console.log('script end')
複製代碼複製代碼
node環境下: script start -> async1 start -> async2 -> promise1 -> script end -> promise2 -> async1 end -> setTimeout
Chrome環境下: script start -> async1 start -> async2 -> promise1 -> script end -> async1 end -> promise2 -> setTimeout
按照上面寫的js執行順序就能夠獲得正確結果,但最後卻又存在兩個答案,爲何會出現兩種結果呢?咱們能夠看到兩種結果中就是async1 end 和 Promise2之間的順序出現差異,這主要是V8最新版本與稍老版本的差別,他們對await的執行方法不一樣,以下:
async function f(){
await p
console.log(1);
}
//新版V8應該會解析成下面這樣
function f(){
Promise.resolve(p).then(()=>{
console.log(1)
})
}
//舊版的V8應該會解析成下面的這樣
function f(){
new Promise(resolve=>{
resolve(p)
}).then(()=>{
console.log(1)
})
}
複製代碼複製代碼
正對上面的這兩種差別主要是:
- 當Promise.resolve 的參數爲 promise 對象時直接返回這個 Promise 對象,then 函數在這個 Promise 對象發生改變後馬上執行。
- 舊版的解析 await 時會從新生成一個Promise對象。儘管該 promise 肯定會 resolve 爲 p,但這個過程自己是異步的,也就是如今進入隊列的是新 promise 的 resolve 過程,因此該 promise 的 then 不會被當即調用,而要等到當前隊列執行到前述 resolve 過程纔會被調用,而後再執行then函數。(下面的例子會講解當resolve()參數爲promise時會怎麼執行)
不用擔憂這個題沒解,真相只有一個。根據 TC39 最近決議,await將直接使用 Promise.resolve() 相同語義。
最後咱們以最新決議來分析這個題目的可能的執行過程:
- 定義函數async一、async2。輸出'script start'
- 將 setTimeout 裏面的回調函數(宏任務)添加到下一輪任務隊列。由於這段代碼前面沒有執行任何的異步操做且等待時間爲0s。因此回調函數會被馬上放到下一輪任務隊列的開頭。
- 執行async1。咱們知道async函數裏面await標記以前的語句和 await 後面的語句是同步執行的。因此這裏前後輸出"async1 start",’async2 start‘.
- 這時暫停執行下面的語句,下面的語句被放到當前隊列的最後。
- 繼續執行同步任務。
- 輸出 ‘Promise1’。將then裏面的函數放在當前隊列的最後。
- 而後輸出‘script end’,注意這時只是同步任務執行完了,當前任務隊列的任務尚未執行完畢,還有兩個微任務被添加進來了!隊列是先進先出的結構,因此這裏先輸出 ‘async1 end’ 再輸出 ‘Promise2’,這時第一輪任務隊列才真算執行完了。
- 而後執行下一個任務列表的任務。執行setTimeout裏面的異步函數。輸出‘setTimeout’。
練習一下
stackoverflow上的一道題目
let resolvePromise = new Promise(resolve => {
let resolvedPromise = Promise.resolve()
resolve(resolvedPromise)
})
resolvePromise.then(() => {
console.log('resolvePromise resolved')
})
let resolvedPromiseThen = Promise.resolve().then(res => {
console.log('promise1')
})
resolvedPromiseThen
.then(() => {
console.log('promise2')
})
.then(() => {
console.log('promise3')
})
複製代碼複製代碼
結果:promise1 -> promise2 -> resolvePromise resolved -> promise3
這道題真的是很是費解了。爲何'resolvePromise resolved'會在第三行才顯示呢?和舍友討論了一夜無果。
其實這個題目的難點就在於resolve一個Promise對象,js引擎會怎麼處理。咱們知道Promise.resolve()的參數爲Promise對象時,會直接返回這個Promise對象。但當resolve()的參數爲Promise對象時,狀況會有所不一樣:
resolve(resolvedPromise)
//等同於:
Promise.resolve().then(() => resolvedPromise.then(resolve, reject));
複製代碼複製代碼
因此這裏第一次執行到這兒的時候:
() => resolvedPromise.then(resolve, reject)
會被放入當前任務列表的最後- 而後是Promise1被放入任務列表。
- 沒有同步操做了開始執行微任務列表,這時resolvedPromise是一個已經resolved的Promise直接執行then函數,將resole()函數放入當前隊列的最後,輸出Promise1。
- 將Promise2放入隊列的最後。執行resole()
- 這時的resolvePromise終於變成了一個resolved狀態的Promise對象了,將‘resolvePromise resolved’放入當前任務列表的最後。輸出Promise2。
- 將Promise3放到當前任務隊列的最後。輸出resolvePromise resolved。輸出Promise3.
結束!這裏面的幾段代碼是比較重要的,解釋了js會按照什麼樣的方式來執行這些新特性。
最後若是有誤,歡迎指正。
參考:
經過microtasks和macrotasks看JavaScript異步任務執行順序
JavaScript 運行機制詳解:再談Event Loop
async/await 在chrome 環境和 node 環境的 執行結果不一致,求解?
What's the difference between resolve(thenable) and resolve('non-thenable-object')?