Promise、Generator、async有什么區(qū)別?
前言
我們知道Promise與Async/await函數(shù)都是用來(lái)解決JavaScript中的異步問(wèn)題的,從最開(kāi)始的回調(diào)函數(shù)處理異步,到Promise處理異步,到Generator處理異步,再到Async/await處理異步,每一次的技術(shù)更新都使得JavaScript處理異步的方式更加優(yōu)雅,從目前來(lái)看,Async/await被認(rèn)為是異步處理的終極解決方案,讓JS的異步處理越來(lái)越像同步任務(wù)。「異步編程的最高境界,就是根本不用關(guān)心它是不是異步」。
異步解決方案的發(fā)展歷程
1.回調(diào)函數(shù)
從早期的Javascript代碼來(lái)看,在ES6誕生之前,基本上所有的異步處理都是基于回調(diào)函數(shù)函數(shù)實(shí)現(xiàn)的,你們可能會(huì)見(jiàn)過(guò)下面這種代碼:
ajax('aaa',?()?=>?{
????//?callback?函數(shù)體
????ajax('bbb',?()?=>?{
????????//?callback?函數(shù)體
????????ajax('ccc',?()?=>?{
????????????//?callback?函數(shù)體
????????})
????})
})
沒(méi)錯(cuò),在ES6出現(xiàn)之前,這種代碼可以說(shuō)是隨處可見(jiàn)。它雖然解決了異步執(zhí)行的問(wèn)題,可隨之而來(lái)的是我們常聽(tīng)說(shuō)的「回調(diào)地獄」問(wèn)題:
沒(méi)有順序可言:嵌套函數(shù)執(zhí)行帶來(lái)的是調(diào)試?yán)щy,不利于維護(hù)與閱讀 耦合性太強(qiáng):一旦某一個(gè)嵌套層級(jí)有改動(dòng),就會(huì)影響整個(gè)回調(diào)的執(zhí)行
「所以,為了解決這個(gè)問(wèn)題,社區(qū)最早提出和實(shí)現(xiàn)了Promise,ES6將其寫(xiě)進(jìn)了語(yǔ)言標(biāo)準(zhǔn),統(tǒng)一了用法?!?/strong>
2.Promise
Promise 是異步編程的一種解決方案,比傳統(tǒng)的解決方案——回調(diào)函數(shù)和事件——更合理和更強(qiáng)大。它就是為了解決回調(diào)函數(shù)產(chǎn)生的問(wèn)題而誕生的。
有了Promise對(duì)象,就可以將異步操作以同步操作的流程表達(dá)出來(lái),避免了層層嵌套的回調(diào)函數(shù)。此外,Promise對(duì)象提供統(tǒng)一的接口,使得控制異步操作更加容易。
所以上面那種回調(diào)函數(shù)的方式我們可以改成這樣:(前提是ajax已用Promise包裝)
ajax('aaa').then(res=>{
??return?ajax('bbb')
}).then(res=>{
??return?ajax('ccc')
})
通過(guò)使用Promise來(lái)處理異步,比以往的回調(diào)函數(shù)看起來(lái)更加清晰了,解決了回調(diào)地獄的問(wèn)題,Promise的then的鏈?zhǔn)秸{(diào)用更能讓人接受,也符合我們同步的思想。
「但Promise也有它的缺點(diǎn):」
Promise的內(nèi)部錯(cuò)誤使用 try catch捕獲不到,只能只用then的第二個(gè)回調(diào)或catch來(lái)捕獲
let?pro
try{
????pro?=?new?Promise((resolve,reject)?=>?{
????????throw?Error('err....')
????})
}catch(err){
????console.log('catch',err)?//?不會(huì)打印
}
pro.catch(err=>{
????console.log('promise',err)?//?會(huì)打印
})
Promise一旦新建就會(huì)立即執(zhí)行,無(wú)法取消
之前寫(xiě)過(guò)一篇從如何使用到如何實(shí)現(xiàn)一個(gè)Promise,講解了Promise如何使用以及內(nèi)部實(shí)現(xiàn)原理。對(duì)Promise還不太理解的同學(xué)可以看看~
3.Generator
Generator 函數(shù)是 ES6 提供的一種異步編程解決方案,語(yǔ)法行為與傳統(tǒng)函數(shù)完全不同。Generator 函數(shù)將 JavaScript 異步編程帶入了一個(gè)全新的階段。
聲明
與函數(shù)聲明類(lèi)似,不同的是function關(guān)鍵字與函數(shù)名之間有一個(gè)星號(hào),以及函數(shù)體內(nèi)部使用yield表達(dá)式,定義不同的內(nèi)部狀態(tài)(yield在英語(yǔ)里的意思就是“產(chǎn)出”)。
function*?gen(x){
?const?y?=?yield?x?+?6;
?return?y;
}
//?yield?如果用在另外一個(gè)表達(dá)式中,要放在()里面
//?像上面如果是在=右邊就不用加()
function*?genOne(x){
??const?y?=?`這是第一個(gè)?yield?執(zhí)行:${yield?x?+?1}`;
?return?y;
}
執(zhí)行
const?g?=?gen(1);
//執(zhí)行?Generator?會(huì)返回一個(gè)Object,而不是像普通函數(shù)返回return?后面的值
g.next()?//?{?value:?7,?done:?false?}
//調(diào)用指針的?next?方法,會(huì)從函數(shù)的頭部或上一次停下來(lái)的地方開(kāi)始執(zhí)行,直到遇到下一個(gè)?yield?表達(dá)式或return語(yǔ)句暫停,也就是執(zhí)行yield?這一行
//?執(zhí)行完成會(huì)返回一個(gè)?Object,
//?value?就是執(zhí)行?yield?后面的值,done?表示函數(shù)是否執(zhí)行完畢
g.next()?//?{?value:?undefined,?done:?true?}
//?因?yàn)樽詈笠恍?return?y?被執(zhí)行完成,所以done?為?true
調(diào)用 Generator 函數(shù)后,該函數(shù)并不執(zhí)行,返回的也不是函數(shù)運(yùn)行結(jié)果,而是一個(gè)指向內(nèi)部狀態(tài)的指針對(duì)象,也就是遍歷器對(duì)象(Iterator Object)。下一步,必須調(diào)用遍歷器對(duì)象的next方法,使得指針移向下一個(gè)狀態(tài)。
所以上面的回調(diào)函數(shù)又可以寫(xiě)成這樣:
function?*fetch()?{
????yield?ajax('aaa')
????yield?ajax('bbb')
????yield?ajax('ccc')
}
let?gen?=?fetch()
let?res1?=?gen.next()?//?{?value:?'aaa',?done:?false?}
let?res2?=?gen.next()?//?{?value:?'bbb',?done:?false?}
let?res3?=?gen.next()?//?{?value:?'ccc',?done:?false?}
let?res4?=?gen.next()?//?{?value:?undefined,?done:?true?}?done為true表示執(zhí)行結(jié)束
由于 Generator 函數(shù)返回的遍歷器對(duì)象,只有調(diào)用next方法才會(huì)遍歷下一個(gè)內(nèi)部狀態(tài),所以其實(shí)提供了一種可以暫停執(zhí)行的函數(shù)。yield表達(dá)式就是暫停標(biāo)志。
遍歷器對(duì)象的next方法的運(yùn)行邏輯如下。
(1)遇到yield表達(dá)式,就暫停執(zhí)行后面的操作,并將緊跟在yield后面的那個(gè)表達(dá)式的值,作為返回的對(duì)象的value屬性值。
(2)下一次調(diào)用next方法時(shí),再繼續(xù)往下執(zhí)行,直到遇到下一個(gè)yield表達(dá)式。
(3)如果沒(méi)有再遇到新的yield表達(dá)式,就一直運(yùn)行到函數(shù)結(jié)束,直到return語(yǔ)句為止,并將return語(yǔ)句后面的表達(dá)式的值,作為返回的對(duì)象的value屬性值。
(4)如果該函數(shù)沒(méi)有return語(yǔ)句,則返回的對(duì)象的value屬性值為undefined。
「yield表達(dá)式本身沒(méi)有返回值,或者說(shuō)總是返回undefined。next方法可以帶一個(gè)參數(shù),該參數(shù)就會(huì)被當(dāng)作上一個(gè)yield表達(dá)式的返回值?!?/strong>
怎么理解這句話(huà)?我們來(lái)看下面這個(gè)例子:
function*?foo(x)?{
??var?y?=?2?*?(yield?(x?+?1));
??var?z?=?yield?(y?/?3);
??return?(x?+?y?+?z);
}
var?a?=?foo(5);
a.next()?//?Object{value:6,?done:false}
a.next()?//?Object{value:NaN,?done:false}
a.next()?//?Object{value:NaN,?done:true}
var?b?=?foo(5);
b.next()?//?{?value:6,?done:false?}
b.next(12)?//?{?value:8,?done:false?}
b.next(13)?//?{?value:42,?done:true?}
由于yield沒(méi)有返回值,所以(yield(x+1))執(zhí)行后的值是undefined,所以在第二次執(zhí)行a.next()是其實(shí)是執(zhí)行的2*undefined,所以值是NaN,所以下面b的例子中,第二次執(zhí)行b.next()時(shí)傳入了12,它會(huì)當(dāng)成第一次b.next()的執(zhí)行返回值,所以b的例子中能夠正確計(jì)算。「這里不能把next執(zhí)行結(jié)果中的value值與yield返回值搞混了,它兩不是一個(gè)東西」
yield與return的區(qū)別
相同點(diǎn):
都能返回語(yǔ)句后面的那個(gè)表達(dá)式的值 都可以暫停函數(shù)執(zhí)行
區(qū)別:
一個(gè)函數(shù)可以有多個(gè) yield,但是只能有一個(gè) return yield 有位置記憶功能,return 沒(méi)有
4.Async/await
Async/await其實(shí)就是上面Generator的語(yǔ)法糖,async函數(shù)其實(shí)就相當(dāng)于funciton *的作用,而await就相當(dāng)與yield的作用。而在async/await機(jī)制中,自動(dòng)包含了我們上述封裝出來(lái)的spawn自動(dòng)執(zhí)行函數(shù)。
所以上面的回調(diào)函數(shù)又可以寫(xiě)的更加簡(jiǎn)潔了:
async?function?fetch()?{
???await?ajax('aaa')
????await?ajax('bbb')
????await?ajax('ccc')
}
//?但這是在這三個(gè)請(qǐng)求有相互依賴(lài)的前提下可以這么寫(xiě),不然會(huì)產(chǎn)生性能問(wèn)題,因?yàn)槟忝恳粋€(gè)請(qǐng)求都需要等待上一次請(qǐng)求完成后再發(fā)起請(qǐng)求,如果沒(méi)有相互依賴(lài)的情況下,建議讓它們同時(shí)發(fā)起請(qǐng)求,這里可以使用Promise.all()來(lái)處理
async函數(shù)對(duì)Generator函數(shù)的改進(jìn),體現(xiàn)在以下四點(diǎn):
內(nèi)置執(zhí)行器: async函數(shù)執(zhí)行與普通函數(shù)一樣,不像Generator函數(shù),需要調(diào)用next方法,或使用co模塊才能真正執(zhí)行語(yǔ)意化更清晰: async和await,比起星號(hào)和yield,語(yǔ)義更清楚了。async表示函數(shù)里有異步操作,await表示緊跟在后面的表達(dá)式需要等待結(jié)果。適用性更廣: co模塊約定,yield命令后面只能是 Thunk 函數(shù)或 Promise 對(duì)象,而async函數(shù)的await命令后面,可以是 Promise 對(duì)象和原始類(lèi)型的值(數(shù)值、字符串和布爾值,但這時(shí)會(huì)自動(dòng)轉(zhuǎn)成立即 resolved 的 Promise 對(duì)象)。返回值是Promise: async函數(shù)的返回值是 Promise 對(duì)象,這比 Generator 函數(shù)的返回值是 Iterator 對(duì)象方便多了。你可以用then方法指定下一步的操作。
async函數(shù)
async函數(shù)的返回值為Promise對(duì)象,所以它可以調(diào)用then方法
async?function?fn()?{
??return?'async'
}
fn().then(res?=>?{
??console.log(res)?//?'async'
})
await表達(dá)式
「await」 右側(cè)的表達(dá)式一般為 「promise」 對(duì)象, 但也可以是其它的值
如果表達(dá)式是 promise 對(duì)象, await 返回的是 promise 成功的值 如果表達(dá)式是其它值, 直接將此值作為 await 的返回值 await后面是Promise對(duì)象會(huì)阻塞后面的代碼,Promise 對(duì)象 resolve,然后得到 resolve 的值,作為 await 表達(dá)式的運(yùn)算結(jié)果 所以這就是await必須用在async的原因,async剛好返回一個(gè)Promise對(duì)象,可以異步執(zhí)行阻塞
function?fn()?{
????return?new?Promise((resolve,?reject)?=>?{
????????setTimeout(()?=>?{
????????????resolve(1000)
????????},?1000);
????})
}
function?fn1()?{?return?'nanjiu'?}
async?function?fn2()?{
????//?const?value?=?await?fn()?//?await?右側(cè)表達(dá)式為Promise,得到的結(jié)果就是Promise成功的value
????//?const?value?=?await?'南玖'
????const?value?=?await?fn1()
????console.log('value',?value)
}
fn2()?//?value?'nanjiu'
異步方案比較
后三種方案都是為解決傳統(tǒng)的回調(diào)函數(shù)而提出的,所以它們相對(duì)于回調(diào)函數(shù)的優(yōu)勢(shì)不言而喻。而async/await又是Generator函數(shù)的語(yǔ)法糖。
Promise的內(nèi)部錯(cuò)誤使用 try catch捕獲不到,只能只用then的第二個(gè)回調(diào)或catch來(lái)捕獲,而async/await的錯(cuò)誤可以用try catch捕獲Promise一旦新建就會(huì)立即執(zhí)行,不會(huì)阻塞后面的代碼,而async函數(shù)中await后面是Promise對(duì)象會(huì)阻塞后面的代碼。async函數(shù)會(huì)隱式地返回一個(gè)promise,該promise的reosolve值就是函數(shù)return的值。使用 async函數(shù)可以讓代碼更加簡(jiǎn)潔,不需要像Promise一樣需要調(diào)用then方法來(lái)獲取返回值,不需要寫(xiě)匿名函數(shù)處理Promise的resolve值,也不需要定義多余的data變量,還避免了嵌套代碼。
說(shuō)了這么多,順便看個(gè)題吧~
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')
「解析:」
打印順序應(yīng)該是:script start -> async2 end -> Promise -> script end -> async1 end -> promise1 -> promise2 -> setTimeout
老規(guī)矩,全局代碼自上而下執(zhí)行,先打印出script start,然后執(zhí)行async1(),里面先遇到await async2(),執(zhí)行async2,打印出async2 end,然后await后面的代碼放入微任務(wù)隊(duì)列,接著往下執(zhí)行new Promise,打印出Promise,遇見(jiàn)了resolve,將第一個(gè)then方法放入微任務(wù)隊(duì)列,接著往下執(zhí)行打印出script end,全局代碼執(zhí)行完了,然后從微任務(wù)隊(duì)列中取出第一個(gè)微任務(wù)執(zhí)行,打印出async1 end,再取出第二個(gè)微任務(wù)執(zhí)行,打印出promise1,然后這個(gè)then方法執(zhí)行完了,當(dāng)前Promise的狀態(tài)為fulfilled,它也可以出發(fā)then的回調(diào),所以第二個(gè)then這時(shí)候又被加進(jìn)了微任務(wù)隊(duì)列,然后再出微任務(wù)隊(duì)列中取出這個(gè)微任務(wù)執(zhí)行,打印出promise2,此時(shí)微任務(wù)隊(duì)列為空,接著執(zhí)行宏任務(wù)隊(duì)列,打印出setTimeout。
「解題技巧:」
無(wú)論是then還是catch里的回調(diào)內(nèi)容只要代碼正常執(zhí)行或者正常返回,則當(dāng)前新的Promise實(shí)例為fulfilled狀態(tài)。如果有報(bào)錯(cuò)或返回Promise.reject()則新的Promise實(shí)例為rejected狀態(tài)。 fulfilled狀態(tài)能夠觸發(fā)then回調(diào) rejected狀態(tài)能夠觸發(fā)catch回調(diào) 執(zhí)行async函數(shù),返回的是Promise對(duì)象 await相當(dāng)于Promise的then并且同一作用域下await下面的內(nèi)容全部作為then中回調(diào)的內(nèi)容 異步中先執(zhí)行微任務(wù),再執(zhí)行宏任務(wù)
最后
如果你覺(jué)得這篇內(nèi)容對(duì)你挺有啟發(fā),我想邀請(qǐng)你幫我三個(gè)小忙:
點(diǎn)個(gè)「在看」,讓更多的人也能看到這篇內(nèi)容(喜歡不點(diǎn)在看,都是耍流氓 -_-)
歡迎加我微信「qianyu443033099」拉你進(jìn)技術(shù)群,長(zhǎng)期交流學(xué)習(xí)...
關(guān)注公眾號(hào)「前端下午茶」,持續(xù)為你推送精選好文,也可以加我為好友,隨時(shí)聊騷。

