相對于回調(diào)函數(shù)來說,Promise是一種相對優(yōu)雅的選擇。那么有沒有更好的方案呢?答案就是async/await。
優(yōu)勢主要體現(xiàn)在,級聯(lián)調(diào)用,也就是幾個調(diào)用依次發(fā)生的場景。
async/await。被稱為到目前最優(yōu)雅的異步過程解決方案,不知道你是否認(rèn)同,反正我是信了。
相對于Promise,async/await有什么優(yōu)點(diǎn)?
比較場景: 級聯(lián)調(diào)用,也就是幾個調(diào)用依次發(fā)生的場景
Promise主要用then函數(shù)的鏈?zhǔn)秸{(diào)用,一直點(diǎn)點(diǎn)點(diǎn),是一種從左向右的橫向?qū)懛ā?br>async/await從上到下,順序執(zhí)行,就像寫同步代碼一樣。這更符合人編寫代碼的習(xí)慣Promise的then函數(shù)只能傳遞一個參數(shù),雖然可以通過包裝成對象,但是這會導(dǎo)致傳遞冗余信息,頻繁的解析又重新組合參數(shù),比較麻煩。
async/await沒有這個限制,就當(dāng)做普通的局部變量來處理好了,用let或者const定義的塊級變量,想怎么用就怎么用,想定義幾個就定義幾個,完全沒有限制,也沒有冗余的工作。Promise在使用的時候最好將同步代碼和異步代碼放在不同的then節(jié)點(diǎn)中,這樣結(jié)構(gòu)更加清晰。
async/await整個書寫習(xí)慣都是同步的,不需要糾結(jié)同步和異步的區(qū)別。當(dāng)然,異步過程需要包裝成一個Promise對象,放在await關(guān)鍵字后面,這點(diǎn)還是要牢記的。Promise是根據(jù)函數(shù)式編程的范式,對異步過程進(jìn)行了一層封裝。
async/await是基于協(xié)程的機(jī)制,是真正的“保存上下文,控制權(quán)切換 ... ... 控制權(quán)恢復(fù),取回上下文”這種機(jī)制,是對異步過程更精確的一種描述。
進(jìn)程、線程和協(xié)程的理解
上面的文章很好地解釋了這幾個概念的區(qū)別。
如果不糾結(jié)細(xì)節(jié),可以簡單地認(rèn)為:進(jìn)程 > 線程 > 協(xié)程;
協(xié)程可以獨(dú)立完成一些與界面無關(guān)的工作,不會阻塞主線程渲染界面,也就是不會卡。
協(xié)程,雖然小一點(diǎn),不過能完成我們程序員交給的任務(wù)。而且我們可以自由控制運(yùn)行和阻塞狀態(tài),不需要求助于高大上的系統(tǒng)調(diào)度,這才是重點(diǎn)。
-
async/await是基于Promise的,是進(jìn)一步的一種優(yōu)化。不過再寫代碼的時候,Promise本身的API出現(xiàn)得很少,很接近同步代碼的寫法。
await關(guān)鍵字使用時有哪些注意點(diǎn)?
只能放在
async函數(shù)內(nèi)部使用,不能放在普通函數(shù)里面,否則會報錯。后面放
Promise對象,在Pending狀態(tài)時,相應(yīng)的協(xié)程會交出控制權(quán),進(jìn)入等待狀態(tài)。這個是本質(zhì)。await是async wait的意思,wait的是resolve(data)消息,并把數(shù)據(jù)data返回。比如,下面代碼中,當(dāng)Promise對象由Pending變?yōu)?code>Resolved的時候,變量a就等于data;然后再順序執(zhí)行下面的語句console.log(a);
這真的是等待,真的是順序執(zhí)行,表現(xiàn)和同步代碼幾乎一模一樣。
const a = await new Promise((resolve, reject) => {
// async process ...
return resolve(data);
});
console.log(a);
await后面也可以跟同步代碼,不過系統(tǒng)會自動轉(zhuǎn)化成一個Promise對象。
比如
const a = await 'hello world';
其實就相當(dāng)于
const a = await Promise.resolve('hello world');
這跟同步代碼
const a = 'hello world';是一樣的,還不如省點(diǎn)事,去掉這里的await關(guān)鍵字。await只關(guān)心異步過程成功的消息resolve(data),拿到相應(yīng)的數(shù)據(jù)data。至于失敗消息reject(error),不關(guān)心,不處理。
當(dāng)然對于錯誤消息的處理,有以下幾種方法供選擇:
(1)讓await后面的Promise對象自己catch
(2)也可以讓外面的async函數(shù)返回的Promise對象統(tǒng)一catch
(3)像同步代碼一樣,放在一個try...catch結(jié)構(gòu)中
async關(guān)鍵字使用時有哪些注意點(diǎn)?
有了這個
async關(guān)鍵字,只是表明里面可能有異步過程,里面可以有await關(guān)鍵字。當(dāng)然,全部是同步代碼也沒關(guān)系。當(dāng)然,這時候這個async關(guān)鍵字就顯得多余了。不是不能加,而是不應(yīng)該加。async函數(shù),如果里面有異步過程,會等待;
但是async函數(shù)本身會馬上返回,不會阻塞當(dāng)前線程。
可以簡單認(rèn)為,
async函數(shù)工作在主線程,同步執(zhí)行,不會阻塞界面渲染。
async函數(shù)內(nèi)部由async關(guān)鍵字修飾的異步過程,工作在相應(yīng)的協(xié)程上,會阻塞等待異步任務(wù)的完成再返回。
async函數(shù)的返回值是一個Promise對象,這個是和普通函數(shù)本質(zhì)不同的地方。這也是使用時重點(diǎn)注意的地方
(1)return newPromise();這個符合async函數(shù)本意;
(2)return data;這個是同步函數(shù)的寫法,這里是要特別注意的。這個時候,其實就相當(dāng)于Promise.resolve(data);還是一個Promise對象。
在調(diào)用async函數(shù)的地方通過簡單的=是拿不到這個data的。
那么怎么樣拿到這個data呢?
很簡單,返回值是一個Promise對象,用.then(data => { })函數(shù)就可以。
(3)如果沒有返回,相當(dāng)于返回了Promise.resolve(undefined);await是不管異步過程的reject(error)消息的,async函數(shù)返回的這個Promise對象的catch函數(shù)就負(fù)責(zé)統(tǒng)一抓取內(nèi)部所有異步過程的錯誤。
async函數(shù)內(nèi)部只要有一個異步過程發(fā)生錯誤,整個執(zhí)行過程就中斷,這個返回的Promise對象的catch就能抓到這個錯誤。async函數(shù)執(zhí)行和普通函數(shù)一樣,函數(shù)名帶個()就可以了,參數(shù)個數(shù)隨意,沒有限制;也需要有async關(guān)鍵字。
只是返回值是一個Promise對象,可以用then函數(shù)得到返回值,用catch抓去整個流程中發(fā)生的錯誤。
基本套路
Step1:用Promise對象包裝異步過程,這個和Promise的使用一樣。只是參數(shù)個數(shù)隨意,沒有限制。
function sleep(ms) {
return new Promise((resolve) => {
setTimeout(() => {
resolve('sleep for ' + ms + ' ms');
}, ms);
});
}
Step2:定義異步流程,可以將按照需要定制,就像寫同步代碼那樣
async function asyncFunction() {
console.time('asyncFunction total executing:');
const sleep1 = await sleep(2000);
console.log('sleep1: ' + sleep1);
const [sleep2, sleep3, sleep4]= await Promise.all([sleep(2000), sleep(1000), sleep(1500)]);
console.log('sleep2: ' + sleep2);
console.log('sleep3: ' + sleep3);
console.log('sleep4: ' + sleep4);
const sleepRace = await Promise.race([sleep(3000), sleep(1000), sleep(1000)]);
console.log('sleep race: ' + sleepRace);
console.timeEnd('asyncFunction total executing:');
return 'asyncFunction done.' // 這個可以不返回,這里只是做個標(biāo)記,為了顯示流程
}
Step3:像普通函數(shù)調(diào)用async函數(shù),在then函數(shù)中獲取整個流程的返回信息,在catch函數(shù)統(tǒng)一處理出錯信息
asyncFunction().then(data => {
console.log(data); // asyncFunction return 的內(nèi)容在這里獲取
}).catch(error => {
console.log(error); // asyncFunction 的錯誤統(tǒng)一在這里抓取
});
console.log('after asyncFunction code executing....'); // 這個代表asyncFunction函數(shù)后的代碼,
// 顯示asyncFunction本身會立即返回,不會阻塞主線程
流程解析
上面的代碼執(zhí)行之后,輸出的log如下,顯示了代碼執(zhí)行流程
after asyncFunction code executing....
sleep1: sleep for 2000 ms
sleep2: sleep for 2000 ms
sleep3: sleep for 1000 ms
sleep4: sleep for 1500 ms
sleep race: sleep for 1000 ms
asyncFunction total executing:: 5006.276123046875ms
asyncFunction done.
after asyncFunction code executing....代碼位置在async函數(shù)asyncFunction()調(diào)用之后,反而先輸出。這說明async函數(shù)asyncFunction()調(diào)用之后會馬上返回,不會阻塞主線程。sleep1: sleep for 2000 ms這是第一個await之后的第一個異步過程,最先執(zhí)行,也最先完成,說明后面的代碼,不論是同步和異步,都在等他執(zhí)行完畢。sleep2 ~ sleep4這是第二個await之后的Promise.all()異步過程。這是“比慢模式”,三個sleep都完成后,再運(yùn)行下面的代碼,耗時最長的是2000ms;sleep race: sleep for 1000 ms這是第三個await之后的Promise.race()異步過程。這是“比快模式”,耗時最短sleep都完成后,就運(yùn)行下面的代碼。耗時最短的是1000ms;asyncFunction total executing:: 5006.276123046875ms這是最后的統(tǒng)計總共運(yùn)行時間代碼。三個await之后的異步過程之和
1000(獨(dú)立的) + 2000(Promise.all) + 1000(Promise.race) = 5000ms
這個和統(tǒng)計出來的5006.276123046875ms非常接近。說明上面的異步過程,和同步代碼執(zhí)行過程一致,協(xié)程真的是在等待異步過程執(zhí)行完畢。asyncFunction done.這個是async函數(shù)返回的信息,在執(zhí)行時的then函數(shù)中獲得,說明整個流程完畢之后參數(shù)傳遞的過程。

異常處理
async標(biāo)注過的函數(shù),返回一個Promise對象,采用.then().catch()的方式來進(jìn)行異常處理,是非常自然的方法,也推薦這么做。就像上面的step3那樣做。另外一種方法,就是對于異步過程采用
await關(guān)鍵字,采用同步的try{} catch(){}的方式來進(jìn)行異常處理。這里要注意的是
await關(guān)鍵字只能用在async標(biāo)注的函數(shù)中,所以,原來的函數(shù),不管以前是同步的還是異步的,都要加上async關(guān)鍵字,比如componentDidMount()就要變?yōu)?code>async componentDidMount()才可以在內(nèi)部使用await關(guān)鍵字,不過功能上沒有任何影響。另外,采用同步的
try{} catch(){}的方式,可以把同步,異步代碼都可以放在里面,有錯誤都能抓到,比如null.length這種,也能抓到。
async componentDidMount() { // 這是React Native的回調(diào)函數(shù),加個async關(guān)鍵字,沒有任何影響,但是可以用await關(guān)鍵字
// 將異步和同步的代碼放在一個try..catch中,異常都能抓到
try {
let array = null;
let data = await asyncFunction(); // 這里用await關(guān)鍵字,就能拿到結(jié)果值;否則,沒有await的話,只能拿到Promise對象
if (array.length > 0) { // 這里會拋出異常,下面的catch也能抓到
array.push(data);
}
} catch (error) {
alert(JSON.stringify(error))
}
}
這里模擬的是網(wǎng)絡(luò)過程。一般情況,
array是一個數(shù)組,用if(array.length > 0)判斷一下長度,有值再處理,沒有問題。但是,一旦網(wǎng)絡(luò)出問題,array就是一個null,平時工作很好的if (array.length > 0)判斷就會拋異常,JS代碼就中斷,停止工作,會帶來意想不到的問題。
這里加了一個try..catch結(jié)構(gòu),這種異常就能捕獲,(這是同步代碼中的異常,不能用.then().catch()抓到),根據(jù)異常信息,一般是null沒有length屬性,方便定位問題。這里的話用if (array && (array.length > 0))就會安全一點(diǎn)。
參考文章
本文只是介紹了async/await一種基礎(chǔ)的用法。一個例子,將三種Promise使用中常用的場景模式都包括進(jìn)去了,并且代碼風(fēng)格和同步代碼非常相似。相比之下async/await這套異步代碼編程方式確實比較優(yōu)雅。
下面幾篇文章都非常不錯,介紹了async/await很多靈活的用法,建議好好看看。