async await的執行 註意: 本次代碼僅在 Chrome 73 下進行測試。 start 不瞭解 async await 的,先去看阮一峰老師的文章 "async 函數" 。 先來看一道 "頭條的面試題" ,這其實是考察瀏覽器的 event loop. 運行結果如下: Event Loop ...
async await的執行
註意:本次代碼僅在 Chrome 73 下進行測試。
start
不瞭解 async await 的,先去看阮一峰老師的文章async 函數。
先來看一道頭條的面試題,這其實是考察瀏覽器的 event loop.
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')
運行結果如下:
script start
async1 start
async2
promise1
script end
async1 end
promise2
setTimeout
Event Loop 淺析
首先 js 是單線程的,所有的任務都在主線程執行,而任務又分為非同步任務和同步任務。
當主線程順序執行所有任務的時候,會執行遇到的同步任務,當遇到非同步任務時,會把它推入一個額外的任務隊列中,直到所有的同步任務執行完畢,才會取出任務隊列中的任務執行。
console.log('同步任務1')
function test () {
console.log('同步任務2')
}
test()
setTimeout(() => console.log('非同步任務3'), 0)
console.log('同步任務4')
執行結果:
同步任務1
同步任務2
同步任務4
// 非同步任務最後才執行
非同步任務3
然而非同步任務還分為微任務(micro-task)和巨集任務(macro-task),不同的非同步任務會進入不同的任務隊列中,而在同步任務執行完畢後,主線程會先取出所有的微任務執行,即清空微任務隊列,然後才取出一個巨集任務執行,接著繼續清空微任務隊列,之後再取出一個巨集任務執行,迴圈直至任務隊列為空,這就是 event loop。
瀏覽器端的巨集任務主要包含:script(整體代碼)、setTimeout、setInterval、I/O、UI交互事件;微任務主要包含:Promise、MutaionObserver(API 用來監視 DOM 變動)
console.log('同步任務1')
function test () {
console.log('同步任務2')
}
test()
setTimeout(() => console.log('非同步巨集任務3'), 0)
function asyncTask () {
return new Promise((resolve, reject)=> {
console.log('同步任務5')
resolve()
})
}
asyncTask().then(() => console.log('非同步微任務6'))
console.log('同步任務4')
運行結果:
同步任務1
同步任務2
同步任務5
同步任務4
// 微任務是先於巨集任務的
非同步微任務6
非同步巨集任務3
註意:promise 裡面的代碼是立即執行的,then 函數才是非同步的。
async await的執行順序
首先 async 是基於 promise 的,async 函數執行完畢會返回一個 promise。
async function asyncFunc () {
return 'hhh'
}
// Promise {<resolved>: "hhh"}
console.log(asyncFunc())
註意:async 函數返回的 Promise 對象,必須等到內部所有await
執行完,才會發生狀態改變,除非遇到return
語句或者拋出錯誤。也就是說,只有async
函數內部的非同步操作執行完,才會執行then
方法指定的回調函數。
在執行 async 函數時,碰到了 await 的話,會立即執行緊跟 await 的語句,然後把後續代碼推入微任務隊列(以這種形式去理解,實際執行並非如此)。
來看這一個例子:
const syn1 = () => console.log(2)
const syn2 = () => new Promise((r, j)=>r()).then(()=>console.log(3))
async function asyncFunc () {
console.log('start')
await console.log(1);
await syn1()
await syn2()
console.log('end')
return 7
}
setTimeout(() => console.log(5), 0)
console.log(0)
asyncFunc().then(v => console.log(v))
new Promise((r, j)=>r()).then(() => console.log(6))
console.log(4)
運行結果:
0
start
1
4
2
6
3
end
7
5
執行順序是這樣的:
首先執行 setTimeout,把函數
() => console.log(5)
推入非同步巨集任務隊列,進行計時 0 毫秒(即使是 0 毫秒也是存在至少 4 毫秒的延遲的。)// 偽代碼 //此時非同步巨集任務隊列 [ 0: () => console.log(5) ]
執行 console.log(0),列印 0
執行 asyncFunc(),進入 asyncFunc 函數
執行 console.log('start'),列印 start
執行 await console.log(1),列印 1
把 asyncFunc 函數後續代碼推入微任務隊列
//此時微任務隊列 [ 0: ( await syn1() await syn2() console.log('end') return 7 ) ]
執行
new Promise((r, j)=>r()).then(() => console.log(6))
,把 then 中的() => console.log(6)
推入微任務隊列//此時微任務隊列 [ 0: ( await syn1() await syn2() console.log('end') return 7 ), 1: () => console.log(6) ]
執行 console.log(4),列印 4
同步任務執行完畢,開始清空微任務隊列
執行微任務
0: ( await syn1() await syn2() console.log('end') return 7 )
,遇到 await,執行 syn1()進入 syn1函數,執行 console.log(2),列印 2,把後續代碼推入微任務隊列
//此時微任務隊列 [ 0: () => console.log(6), 1: ( await syn2() console.log('end') return 7 ) ]
微任務隊列非空,繼續執行微任務
0: () => console.log(6)
,列印 6//此時微任務隊列 [ 0: ( await syn2() console.log('end') return 7 ) ]
微任務隊列非空,繼續執行微任務
0: ( await syn2() console.log('end') return 7 )
,遇到 await,執行 syn2()進入 syn2 函數,執行
new Promise((r, j)=>r()).then(()=>console.log(3))
,把 then 中的() => console.log(3)
推入微任務隊列//此時微任務隊列 [ 0: () => console.log(3) ]
把後續代碼推入微任務隊列
//此時微任務隊列 [ 0: () => console.log(3), 1: ( console.log('end') return 7 ) ]
微任務隊列非空,繼續執行微任務
0: console.log('3')
,列印 3//此時微任務隊列 [ 0: ( console.log('end') return 7 ) ]
微任務隊列非空,繼續執行微任務
0: ( console.log('end') return 7 )
,列印 end,繼續執行return 7
,asyncFunc 函數執行完畢,返回 Promise.resolve(7),執行then(v => console.log(v))
,把v => console.log(v)
推入微任務隊列//此時微任務隊列 [ 0: console.log(7) ]
微任務隊列非空,繼續執行微任務
0: console.log(7)
,列印 7微任務隊列為空,執行巨集任務
0: () => console.log(5)
,列印 5微任務隊列為空,巨集任務隊列為空,執行完畢。
結尾
其實 async await 的執行並非如此,真正的執行方法是 await 會阻塞後面的代碼,讓出線程,先執行 async 函數外面的同步代碼,等同步代碼執行完,再回到 async 內部繼續執行。
讓我們回到開頭的面試題:
// 原 async1,async2
async function async1() {
console.log('async1 start')
await async2()
console.log('async1 end')
}
async function async2() {
console.log('async2')
}
// 轉換成這樣
function async1() {
console.log('async1 start')
Promise.resolve(async2()).then(() => {
console.log('async1 end')
return Promise.resolve()
})
}
function async2() {
console.log('async2')
return Promise.resolve()
}
但以這種形式去理解是不是比規範中那晦澀的英文更好理解呢。
最後附上TC39規範。