作者:ziwei3749
https://segmentfault.com/a/1190000017224799
測試一下自己有沒有必要看
所以我寫這個的文章,主要還是交流學(xué)習(xí)般又,如果您已經(jīng)清楚了eventloop/async/await/promise這些東西呢,可以 break 啦
有說的不對的地方,歡迎留言討論寞蚌,
那么還是先通過一道題自我檢測一下于游,是否有必要繼續(xù)看下去把。
其實(shí)呢忧勿,這是去年一道爛大街的「今日頭條」的面試題杉女。
我覺得這道題的關(guān)鍵,不僅是說出正確的打印順序鸳吸,更重要的能否說清楚每一個步驟熏挎,為什么這樣執(zhí)行。
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' )
注:因?yàn)槭且坏狼岸嗣嬖囶}晌砾,所以答案是以瀏覽器的eventloop機(jī)制為準(zhǔn)的坎拐,在node平臺上運(yùn)行會有差異。
script start
async1 start
async2
promise1
script end
promise2
async1 end
setTimeout
如果你發(fā)現(xiàn)運(yùn)行結(jié)果跟自己想的一樣养匈,可以選擇跳過這篇文章啦哼勇,
或者如果你有興趣看看俺倆的理解有沒有區(qū)別,可以跳到后面的 「畫圖講解的部分」
需要具備的前置知識
- promise的使用經(jīng)驗(yàn)
- 瀏覽器端的eventloop
不過如果是對 ES7 的 async 不太熟悉呕乎,是沒關(guān)系的哈积担,因?yàn)檫@篇文章會詳解 async。
那么如果不具備這些知識呢猬仁,推薦幾篇我覺得講得比較清楚的文章
- https://segmentfault.com/a/11... 這是我之前寫的講解eventloop的文章帝璧,我覺得還算清晰先誉,但是沒涉及 async
- https://segmentfault.com/a/11... 這是我讀過的講async await最清楚的文章
- http://es6.ruanyifeng.com/#do... promise就推薦阮一峰老師的ES6吧,不過不熟悉 promise 的應(yīng)該較少啦聋溜。
主要內(nèi)容
第1部分:對于async await的理解
我推薦的那篇文章谆膳,對 async/await 講得更詳細(xì)。不過我希望自己能更加精煉的幫你理解它們
這部分撮躁,主要會講解 3 點(diǎn)內(nèi)容
- 1.async 做一件什么事情漱病?
- 2.await 在等什么?
- 3.await 等到之后把曼,做了一件什么事情杨帽?
- 4.補(bǔ)充: async/await 比 promise有哪些優(yōu)勢?(回頭補(bǔ)充)
1.async 做一件什么事情嗤军?
一句話概括: 帶 async 關(guān)鍵字的函數(shù)注盈,它使得你的函數(shù)的返回值必定是 promise 對象
也就是
如果async關(guān)鍵字函數(shù)返回的不是promise,會自動用Promise.resolve()包裝
如果async關(guān)鍵字函數(shù)顯式地返回promise叙赚,那就以你返回的promise為準(zhǔn)
這是一個簡單的例子老客,可以看到 async 關(guān)鍵字函數(shù)和普通函數(shù)的返回值的區(qū)別
async function fn1(){
return 123
}
function fn2(){
return 123
}
console.log(fn1())
console.log(fn2())
Promise {<resolved>: 123}
123
所以你看,async 函數(shù)也沒啥了不起的震叮,以后看到帶有 async 關(guān)鍵字的函數(shù)也不用慌張胧砰,你就想它無非就是把return值包裝了一下,其他就跟普通函數(shù)一樣苇瓣。
關(guān)于async關(guān)鍵字還有那些要注意的尉间?
- 在語義上要理解,async表示函數(shù)內(nèi)部有異步操作
- 另外注意击罪,一般 await 關(guān)鍵字要在 async 關(guān)鍵字函數(shù)的內(nèi)部哲嘲,await 寫在外面會報(bào)錯。
2.await 在等什么媳禁?
一句話概括: await等的是右側(cè)「表達(dá)式」的結(jié)果
也就是說眠副,
右側(cè)如果是函數(shù),那么函數(shù)的return值就是「表達(dá)式的結(jié)果」
右側(cè)如果是一個 'hello' 或者什么值竣稽,那表達(dá)式的結(jié)果就是 'hello'
async function async1() {
console.log( 'async1 start' )
await async2()
console.log( 'async1 end' )
}
async function async2() {
console.log( 'async2' )
}
async1()
console.log( 'script start' )
這里注意一點(diǎn)侦啸,可能大家都知道await會讓出線程,阻塞后面的代碼丧枪,那么上面例子中, 'async2' 和 'script start' 誰先打印呢庞萍?
是從左向右執(zhí)行拧烦,一旦碰到await直接跳出, 阻塞async2()的執(zhí)行?
還是從右向左钝计,先執(zhí)行async2后恋博,發(fā)現(xiàn)有await關(guān)鍵字齐佳,于是讓出線程,阻塞代碼呢债沮?
實(shí)踐的結(jié)論是炼吴,從右向左的。先打印async2疫衩,后打印的script start
之所以提一嘴硅蹦,是因?yàn)槲医?jīng)常看到這樣的說法闷煤,「一旦遇到await就立刻讓出線程童芹,阻塞后面的代碼」
這樣的說法,會讓我誤以為鲤拿,await后面那個函數(shù)假褪, async2()也直接被阻塞呢。
3.await 等到之后近顷,做了一件什么事情生音?
那么右側(cè)表達(dá)式的結(jié)果,就是await要等的東西窒升。
等到之后缀遍,對于await來說,分2個情況
- 不是promise對象
- 是promise對象
如果不是 promise , await會阻塞后面的代碼异剥,先執(zhí)行async外面的同步代碼瑟由,同步代碼執(zhí)行完,再回到async內(nèi)部冤寿,把這個非promise的東西歹苦,作為 await表達(dá)式的結(jié)果
如果它等到的是一個 promise 對象,await 也會暫停async后面的代碼督怜,先執(zhí)行async外面的同步代碼殴瘦,等著 Promise 對象 fulfilled,然后把 resolve 的參數(shù)作為 await 表達(dá)式的運(yùn)算結(jié)果号杠。
第2部分:畫圖一步步看清宏任務(wù)蚪腋、微任務(wù)的執(zhí)行過程
我們以開篇的經(jīng)典面試題為例,分析這個例子中的宏任務(wù)和微任務(wù)姨蟋。
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' )
先分享一個我個人理解的宏任務(wù)和微任務(wù)的慨念屉凯,在我腦海中宏任務(wù)和為微任務(wù)如圖所示
也就是「宏任務(wù)」、「微任務(wù)」都是隊(duì)列眼溶。
一段代碼執(zhí)行時悠砚,會先執(zhí)行宏任務(wù)中的同步代碼,
- 如果執(zhí)行中遇到setTimeout之類宏任務(wù)堂飞,那么就把這個setTimeout內(nèi)部的函數(shù)推入「宏任務(wù)的隊(duì)列」中灌旧,下一輪宏任務(wù)執(zhí)行時調(diào)用绑咱。
- 如果執(zhí)行中遇到promise.then()之類的微任務(wù),就會推入到「當(dāng)前宏任務(wù)的微任務(wù)隊(duì)列」中枢泰,在本輪宏任務(wù)的同步代碼執(zhí)行都完成后描融,依次執(zhí)行所有的微任務(wù)1、2衡蚂、3
下面就以面試題為例子窿克,分析這段代碼的執(zhí)行順序.
每次宏任務(wù)和微任務(wù)發(fā)生變化,我都會畫一個圖來表示他們的變化讳窟。
直接打印同步代碼 console.log('script start')
首先是2個函數(shù)聲明让歼,雖然有async關(guān)鍵字,但不是調(diào)用我們就不看丽啡。然后首先是打印同步代碼 console.log('script start')
將setTimeout放入宏任務(wù)隊(duì)列
默認(rèn)<script></script>所包裹的代碼谋右,其實(shí)可以理解為是第一個宏任務(wù),所以這里是宏任務(wù)2
調(diào)用async1补箍,打印 同步代碼 console.log( 'async1 start' )
我們說過看到帶有async關(guān)鍵字的函數(shù)改执,不用害怕,它的僅僅是把return值包裝成了promise坑雅,其他并沒有什么不同的地方辈挂。所以就很普通的打印 console.log( 'async1 start' )
分析一下 await async2()
前文提過await,1.它先計(jì)算出右側(cè)的結(jié)果裹粤,2.然后看到await后终蒂,中斷async函數(shù)
- 先得到await右側(cè)表達(dá)式的結(jié)果。執(zhí)行async2()遥诉,打印同步代碼console.log('async2'), 并且return Promise.resolve(undefined)
- await后拇泣,中斷async函數(shù),先執(zhí)行async外的同步代碼
目前就直接打印 console.log('async2')
被阻塞后矮锈,要執(zhí)行async之外的代碼
執(zhí)行new Promise()霉翔,Promise構(gòu)造函數(shù)是直接調(diào)用的同步代碼,所以 console.log( 'promise1' )
代碼運(yùn)行到promise.then()
代碼運(yùn)行到promise.then()苞笨,發(fā)現(xiàn)這個是微任務(wù)债朵,所以暫時不打印,只是推入當(dāng)前宏任務(wù)的微任務(wù)隊(duì)列中瀑凝。
注意:這里只是把promise2推入微任務(wù)隊(duì)列序芦,并沒有執(zhí)行。微任務(wù)會在當(dāng)前宏任務(wù)的同步代碼執(zhí)行完畢粤咪,才會依次執(zhí)行
打印同步代碼 console.log( 'script end' )
沒什么好說的芝加。執(zhí)行完這個同步代碼后,「async外的代碼」終于走了一遍
下面該回到 await 表達(dá)式那里,執(zhí)行await Promise.resolve(undefined)了
回到async內(nèi)部藏杖,執(zhí)行await Promise.resolve(undefined)
這部分可能不太好理解,我盡量表達(dá)我的想法脉顿。
對于 await Promise.resolve(undefined) 如何理解呢蝌麸?
https://developer.mozilla.org...
根據(jù) MDN 原話我們知道
如果一個 Promise 被傳遞給一個 await 操作符,await 將等待 Promise 正常處理完成并返回其處理結(jié)果艾疟。
在我們這個例子中来吩,就是Promise.resolve(undefined)正常處理完成,并返回其處理結(jié)果蔽莱。那么await async2()就算是執(zhí)行結(jié)束了弟疆。
目前這個promise的狀態(tài)是fulfilled,等其處理結(jié)果返回就可以執(zhí)行await下面的代碼了盗冷。
那何時能拿到處理結(jié)果呢怠苔?
回憶平時我們用promise,調(diào)用resolve后仪糖,何時能拿到處理結(jié)果柑司?是不是需要在then的第一個參數(shù)里,才能拿到結(jié)果锅劝。
(調(diào)用resolve時攒驰,會把then的參數(shù)推入微任務(wù)隊(duì)列,等主線程空閑時故爵,再調(diào)用它)
所以這里的 await Promise.resolve() 就類似于
Promise.resolve(undefined).then((undefined) => {
})
把then的第一個回調(diào)參數(shù) (undefined) => {} 推入微任務(wù)隊(duì)列玻粪。
then執(zhí)行完,才是await async2()執(zhí)行結(jié)束诬垂。
await async2()執(zhí)行結(jié)束劲室,才能繼續(xù)執(zhí)行后面的代碼
如圖
此時當(dāng)前宏任務(wù)1都執(zhí)行完了,要處理微任務(wù)隊(duì)列里的代碼剥纷。
微任務(wù)隊(duì)列痹籍,先進(jìn)先出的原則,
- 執(zhí)行微任務(wù)1晦鞋,打印promise2
- 執(zhí)行微任務(wù)2蹲缠,沒什么內(nèi)容..
但是微任務(wù)2執(zhí)行后,await async2()語句結(jié)束悠垛,后面的代碼不再被阻塞线定,所以打印
console.log( 'async1 end' )
宏任務(wù)1執(zhí)行完成后,執(zhí)行宏任務(wù)2
宏任務(wù)2的執(zhí)行比較簡單,就是打印
console.log('setTimeout')
補(bǔ)充在不同瀏覽器上的測試結(jié)果
谷歌瀏覽器确买,目前是版本是「版本 71.0.3578.80(正式版本) (64 位)」 Mac操作系統(tǒng)
Safari瀏覽器的測試結(jié)果
火狐瀏覽器的測試結(jié)果
如果不理解可以留言斤讥,有錯誤的話也歡迎指正。
關(guān)于執(zhí)行順序
評論區(qū)有指出
- Chrome72 dev版本的執(zhí)行順序是Promise2后打印,
- 或者是babel編譯過后的代碼是promise2后打印芭商。
我自己也實(shí)踐了一下babel編譯后的代碼執(zhí)行順序的確是promise2后打印的..
原因是ESMA最新規(guī)范的有修改派草,然后這一點(diǎn)的詳情,說實(shí)話我目前也不是很清楚铛楣,評論區(qū)有給出資料近迁,可供參考討論。