摘要:的和我們通過的原型方法拿到我們的返回值輸出我延遲了毫秒后輸出的輸出下列的值我延遲了毫秒后輸出的。有人說,我不想耦合性這么高,想先執(zhí)行函數(shù)再執(zhí)行,但不想用上面那種寫法,可以嗎,答案是當(dāng)然可以。
此文只介紹Async/Await與Promise基礎(chǔ)知識(shí)與實(shí)際用到注意的問題,將通過很多代碼實(shí)例進(jìn)行說明,兩個(gè)實(shí)例代碼是setDelay和setDelaySecond。
tips:本文系原創(chuàng)轉(zhuǎn)自我的博客異步Promise及Async/Await最完整入門攻略,歡迎前端大神交流,指出問題
一、為什么有Async/Await?我們都知道已經(jīng)有了Promise的解決方案了,為什么還要ES7提出新的Async/Await標(biāo)準(zhǔn)呢?
答案其實(shí)也顯而易見:Promise雖然跳出了異步嵌套的怪圈,用鏈?zhǔn)奖磉_(dá)更加清晰,但是我們也發(fā)現(xiàn)如果有大量的異步請(qǐng)求的時(shí)候,流程復(fù)雜的情況下,會(huì)發(fā)現(xiàn)充滿了屏幕的then,看起來非常吃力,而ES7的Async/Await的出現(xiàn)就是為了解決這種復(fù)雜的情況。
首先,我們必須了解Promise。
二、Promise簡(jiǎn)介 2.1 Promise實(shí)例什么是Promise,很多人應(yīng)該都知道基礎(chǔ)概念?直接看下面的代碼(全文的例子都是基于setDelaySecond和setDelay兩個(gè)函數(shù),請(qǐng)務(wù)必記住):
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != "number") reject(new Error("參數(shù)必須是number類型")); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) }
我們把一個(gè)Promise封裝在一個(gè)函數(shù)里面同時(shí)返回了一個(gè)Promise,這樣比較規(guī)范。
可以看到定義的Promise有兩個(gè)參數(shù),resolve和reject。
resolve:將異步的執(zhí)行從pending(請(qǐng)求)變成了resolve(成功返回),是個(gè)函數(shù)執(zhí)行返回。
reject:顧名思義“拒絕”,就是從請(qǐng)求變成了"失敗",是個(gè)函數(shù)可以執(zhí)行返回一個(gè)結(jié)果,但我們這里推薦大家返回一個(gè)錯(cuò)誤new Error()。
上述例子,你可以reject("返回一個(gè)字符串"),隨便你返回,但是我們還是建議返回一個(gè)Error對(duì)象,這樣更加清晰是“失敗的”,這樣更規(guī)范一點(diǎn)。2.2 Promise的then和catch
我們通過Promise的原型方法then拿到我們的返回值:
setDelay(3000) .then((result)=>{ console.log(result) // 輸出“我延遲了2000毫秒后輸出的” })
輸出下列的值:“我延遲了2000毫秒后輸出的”。
如果出錯(cuò)呢?那就用catch捕獲:
setDelay("我是字符串") .then((result)=>{ console.log(result) // 不進(jìn)去了 }) .catch((err)=>{ console.log(err) // 輸出錯(cuò)誤:“參數(shù)必須是number類型” })
是不是很簡(jiǎn)單?好,現(xiàn)在我增加一點(diǎn)難度,如果多個(gè)Promise執(zhí)行會(huì)是怎么樣呢?
2.3 Promise相互依賴我們?cè)趯懸粋€(gè)Promise:
const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != "number" || seconds > 10) reject(new Error("參數(shù)必須是number類型,并且小于等于10")); setTimeout(()=> { console.log(`先是setDelaySeconds函數(shù)輸出,延遲了${seconds}秒,一共需要延遲${seconds+2}秒`) resolve(setDelay(2000)) // 這里依賴上一個(gè)Promise }, seconds * 1000) }) }
在下一個(gè)需要依賴的resolve去返回另一個(gè)Promise,會(huì)發(fā)生什么呢?我們執(zhí)行一下:
setDelaySecond(3).then((result)=>{ console.log(result) }).catch((err)=>{ console.log(err); })
你會(huì)發(fā)現(xiàn)結(jié)果是先執(zhí)行:“先是setDelaySeconds輸出,延遲了2秒,一共需要延遲5秒”
再執(zhí)行setDelay的resolve:“我延遲了2000毫秒后輸出的”。的確做到了依次執(zhí)行的目的。
有人說,我不想耦合性這么高,想先執(zhí)行setDelay函數(shù)再執(zhí)行setDelaySecond,但不想用上面那種寫法,可以嗎,答案是當(dāng)然可以。
2.4 Promise鏈?zhǔn)綄懛?/b>先改寫一下setDelaySecond,拒絕依賴,降低耦合性
const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != "number" || seconds > 10) reject(new Error("參數(shù)必須是number類型,并且小于等于10")); setTimeout(()=> { resolve(`我延遲了${seconds}秒后輸出的,是第二個(gè)函數(shù)`) }, seconds * 1000) }) }
先執(zhí)行setDelay在執(zhí)行setDelaySecond,只需要在第一個(gè)then的結(jié)果中返回下一個(gè)Promise就可以一直鏈?zhǔn)綄懴氯チ耍喈?dāng)于依次執(zhí)行:
setDelay(2000) .then((result)=>{ console.log(result) console.log("我進(jìn)行到第一步的"); return setDelaySecond(3) }) .then((result)=>{ console.log("我進(jìn)行到第二步的"); console.log(result); }).catch((err)=>{ console.log(err); })
發(fā)現(xiàn)確實(shí)達(dá)到了可喜的鏈?zhǔn)剑ńK于脫離異步嵌套苦海,哭),可以看到then的鏈?zhǔn)綄懛ǚ浅?yōu)美。
2.5 鏈?zhǔn)綄懛ㄐ枰⒁獾牡胤?/b>這里一定要提到一點(diǎn):
then式鏈?zhǔn)綄懛ǖ谋举|(zhì)其實(shí)是一直往下傳遞返回一個(gè)新的Promise,也就是說then在下一步接收的是上一步返回的Promise,理解這個(gè)對(duì)于后面的細(xì)節(jié)非常重要??!
那么并不是這么簡(jiǎn)單,then的返回我們可以看出有2個(gè)參數(shù)(都是回調(diào)):
第一個(gè)回調(diào)是resolve的回調(diào),也就是第一個(gè)參數(shù)用得最多,拿到的是上一步的Promise成功resolve的值。
第二個(gè)回調(diào)是reject的回調(diào),用的不多,但是求求大家不要寫錯(cuò)了,通常是拿到上一個(gè)的錯(cuò)誤,那么這個(gè)錯(cuò)誤處理和catch有什么區(qū)別和需要注意的地方呢?
我們修改上面的代碼:
setDelay(2000) .then((result)=>{ console.log(result) console.log("我進(jìn)行到第一步的"); return setDelaySecond(20) }) .then((result)=>{ console.log("我進(jìn)行到第二步的"); console.log(result); }, (_err)=> { console.log("我出錯(cuò)啦,進(jìn)到這里捕獲錯(cuò)誤,但是不經(jīng)過catch了"); }) .then((result)=>{ console.log("我還是繼續(xù)執(zhí)行的?。。?!") }) .catch((err)=>{ console.log(err); })
可以看到輸出結(jié)果是:進(jìn)到了then的第二個(gè)參數(shù)(reject)中去了,而且最重要的是!不再經(jīng)過catch了。
那么我們把catch挪上去,寫到then錯(cuò)誤處理前:
setDelay(2000) .then((result)=>{ console.log(result) console.log("我進(jìn)行到第一步的"); return setDelaySecond(20) }) .catch((err)=>{ // 挪上去了 console.log(err); // 這里catch到上一個(gè)返回Promise的錯(cuò)誤 }) .then((result)=>{ console.log("我進(jìn)行到第二步的"); console.log(result); }, (_err)=> { console.log("我出錯(cuò)啦,但是由于catch在我前面,所以錯(cuò)誤早就被捕獲了,我這沒有錯(cuò)誤了"); }) .then((result)=>{ console.log("我還是繼續(xù)執(zhí)行的?。。?!") })
可以看到先經(jīng)過catch的捕獲,后面就沒錯(cuò)誤了。
可以得出需要注意的:
catch寫法是針對(duì)于整個(gè)鏈?zhǔn)綄懛ǖ腻e(cuò)誤捕獲的,而then第二個(gè)參數(shù)是針對(duì)于上一個(gè)返回Promise的。
兩者的優(yōu)先級(jí):就是看誰在鏈?zhǔn)綄懛ǖ那懊?/strong>,在前面的先捕獲到錯(cuò)誤,后面就沒有錯(cuò)誤可以捕獲了,鏈?zhǔn)角懊娴膬?yōu)先級(jí)大,而且兩者都不是break, 可以繼續(xù)執(zhí)行后續(xù)操作不受影響。
2.5 鏈?zhǔn)綄懛ǖ腻e(cuò)誤處理上述已經(jīng)寫好了關(guān)于then里面三個(gè)回調(diào)中第二個(gè)回調(diào)(reject)會(huì)與catch沖突的問題,那么我們實(shí)際寫的時(shí)候,參數(shù)捕獲的方式基本寫得少,catch的寫法會(huì)用到更多。
既然有了很多的Promise,那么我需不需要寫很多catch呢?
答案當(dāng)然是:不需要!,哪有那么麻煩的寫法,只需要在末尾catch一下就可以了,因?yàn)殒準(zhǔn)綄懛ǖ腻e(cuò)誤處理具有“冒泡”特性,鏈?zhǔn)街腥魏我粋€(gè)環(huán)節(jié)出問題,都會(huì)被catch到,同時(shí)在某個(gè)環(huán)節(jié)后面的代碼就不會(huì)執(zhí)行了。
既然說到這里,我們把catch移到第一個(gè)鏈?zhǔn)降姆祷乩锩鏁?huì)發(fā)生什么事呢?看下面代碼:
setDelay("2000") .then((result)=>{ console.log("第一步完成了"); console.log(result) return setDelaySecond(3) }) .catch((err)=>{ // 這里移到第一個(gè)鏈?zhǔn)饺ィl(fā)現(xiàn)上面的不執(zhí)行了,下面的繼續(xù)執(zhí)行 console.log(err); }) .then((result)=>{ console.log("第二步完成了"); console.log(result); })
驚喜的發(fā)現(xiàn),鏈?zhǔn)嚼^續(xù)走下去了??!輸出如下(undefined是因?yàn)樯弦粋€(gè)then沒有返回一個(gè)Promise):
重點(diǎn)來了!敲黑板?。?strong>鏈?zhǔn)街械?b>catch并不是終點(diǎn)??!catch完如果還有then還會(huì)繼續(xù)往下走!不信的話可以把第一個(gè)catch在最后面的那個(gè)例子后面再加幾個(gè)then,你會(huì)發(fā)現(xiàn)并不會(huì)跳出鏈?zhǔn)綀?zhí)行。
如果順序執(zhí)行setDelay,setDelay1,setDelaySecond,按照上述的邏輯,流程圖可以概括如下:
catch只是捕獲錯(cuò)誤的一個(gè)鏈?zhǔn)奖磉_(dá),并不是break!
所以,catch放的位置也很有講究,一般放在一些重要的、必須catch的程序的最后。**這些重要的程序中間一旦出現(xiàn)錯(cuò)誤,會(huì)馬上跳過其他后續(xù)程序的操作直接執(zhí)行到最近的catch代碼塊,但不影響catch后續(xù)的操作?。。?!
到這就不得不體一個(gè)ES2018標(biāo)準(zhǔn)新引入的Promise的finally,表示在catch后必須肯定會(huì)默認(rèn)執(zhí)行的的操作。這里不多展開,細(xì)節(jié)可以參考:Promise的finally
2.5 Promise鏈?zhǔn)街虚g想返回自定義的值其實(shí)很簡(jiǎn)單,用Promise的原型方法resolve即可:
setDelay(2000).then((result)=>{ console.log("第一步完成了"); console.log(result); let message = "這是我自己想處理的值"; return Promise.resolve(message) // 這里返回我想在下一階段處理的值 }) .then((result)=>{ console.log("第二步完成了"); console.log(result); // 這里拿到上一階段的返回值 //return Promise.resolve("這里可以繼續(xù)返回") }) .catch((err)=>{ console.log(err); })2.7 如何跳出或停止Promise鏈?zhǔn)?/b>
不同于一般的function的break的方式,如果你是這樣的操作:func().then().then().then().catch()的方式,你想在第一個(gè)then就跳出鏈?zhǔn)剑竺娴牟幌雸?zhí)行了,不同于一般的break;return null;return false等操作,可以說,如何停止Promise鏈,是一大難點(diǎn),是整個(gè)Promise最復(fù)雜的地方。
1.用鏈?zhǔn)降乃季S想,我們拒絕掉某一鏈,那么不就是相當(dāng)于直接跳到了catch模塊嗎?
我們是不是可以直接“拒絕“掉達(dá)到停止的目的?
setDelay(2000) .then((result)=>{ console.log(result) console.log("我進(jìn)行到第一步的"); return setDelaySecond(1) }) .then((result)=>{ console.log("我進(jìn)行到第二步的"); console.log(result); console.log("我主動(dòng)跳出循環(huán)了"); return Promise.reject("跳出循環(huán)的信息") // 這里返回一個(gè)reject,主動(dòng)跳出循環(huán)了 }) .then((result)=>{ console.log("我不執(zhí)行"); }) .catch((mes)=>{ console.dir(mes) console.log("我跳出了"); })
但是很容易看到缺點(diǎn):有時(shí)候你并不確定是因?yàn)殄e(cuò)誤跳出的,還是主動(dòng)跳出的,所以我們可以加一個(gè)標(biāo)志位:
return Promise.reject({ isNotErrorExpection: true // 返回的地方加一個(gè)標(biāo)志位,判斷是否是錯(cuò)誤類型,如果不是,那么說明可以是主動(dòng)跳出循環(huán)的 })
或者根據(jù)上述的代碼判斷catch的地方輸出的類型是不是屬于錯(cuò)誤對(duì)象的,是的話說明是錯(cuò)誤,不是的話說明是主動(dòng)跳出的,你可以自己選擇(這就是為什么要統(tǒng)一錯(cuò)誤reject的時(shí)候輸出new Error("錯(cuò)誤信息")的原因,規(guī)范?。?/p>
當(dāng)然你也可以直接拋出一個(gè)錯(cuò)誤跳出:
throw new Error("錯(cuò)誤信息") // 直接跳出,那就不能用判斷是否為錯(cuò)誤對(duì)象的方法進(jìn)行判斷了
2.那有時(shí)候我們有這個(gè)需求:catch是放在中間(不是末尾),而同時(shí)我們又不想執(zhí)行catch后面的代碼,也就是鏈?zhǔn)降慕^對(duì)中止,應(yīng)該怎么辦?
我們看這段代碼:
setDelay(2000) .then((result)=>{ console.log(result) console.log("我進(jìn)行到第一步的"); return setDelaySecond(1) }) .then((result)=>{ console.log("我進(jìn)行到第二步的"); console.log(result); console.log("我主動(dòng)跳出循環(huán)了"); return Promise.reject("跳出循環(huán)的信息") // 這里直接調(diào)用Promise原型方法返回一個(gè)reject,主動(dòng)跳出循環(huán)了 }) .then((result)=>{ console.log("我不執(zhí)行"); }) .catch((mes)=>{ console.dir(mes) console.log("我跳出了"); }) .then((res)=>{ console.log("我不想執(zhí)行,但是卻執(zhí)行了"); // 問題在這,上述的終止方法治標(biāo)不治本。 })
這時(shí)候最后一步then還是執(zhí)行了,整條鏈都其實(shí)沒有本質(zhì)上的跳出,那應(yīng)該怎么辦呢?
敲黑板!!重點(diǎn)來了!我們看Promise/A+規(guī)范可以知道:
A promise must be in one of three states: pending, fulfilled, or rejected.
Promise其實(shí)是有三種狀態(tài)的:pending,resolve,rejected,那么我們一直在討論resolve和rejected這2個(gè)狀態(tài),是不是忽視了pending這個(gè)狀態(tài)呢?pending狀態(tài)顧名思義就是請(qǐng)求中的狀態(tài),成功請(qǐng)求就是resolve,失敗就是reject,其實(shí)他就是個(gè)中間過渡狀態(tài)。
而我們上面討論過了,then的下一層級(jí)其實(shí)得到的是上一層級(jí)返回的Promise對(duì)象,也就是說原Promise對(duì)象與新對(duì)象狀態(tài)保持一致。那么重點(diǎn)來了,如果你想在這一層級(jí)進(jìn)行終止,是不是直接讓它永遠(yuǎn)都pending下去,那么后續(xù)的操作不就沒了嗎?是不是就達(dá)到這個(gè)目的了??覺得有疑問的可以參考Promise/A+規(guī)范。
我們直接看代碼:
setDelay(2000) .then((result)=>{ console.log(result) console.log("我進(jìn)行到第一步的"); return setDelaySecond(1) }) .then((result)=>{ console.log(result); console.log("我主動(dòng)跳出循環(huán)了"); // return Promise.reject("跳出循環(huán)的信息") // 重點(diǎn)在這 return new Promise(()=>{console.log("后續(xù)的不會(huì)執(zhí)行")}) // 這里返回的一個(gè)新的Promise,沒有resolve和reject,那么會(huì)一直處于pending狀態(tài),因?yàn)闆]返回啊,那么這種狀態(tài)就一直保持著,中斷了這個(gè)Promise }) .then((result)=>{ console.log("我不執(zhí)行"); }) .catch((mes)=>{ console.dir(mes) console.log("我跳出了"); }) .then((res)=>{ console.log("我也不會(huì)執(zhí)行") })
這樣就解決了上述,錯(cuò)誤跳出而導(dǎo)致無法完全終止Promise鏈的問題。
但是!隨之而來也有一個(gè)問題,那就是可能會(huì)導(dǎo)致潛在的內(nèi)存泄漏,因?yàn)槲覀冎肋@個(gè)一直處于pending狀態(tài)下的Promise會(huì)一直處于被掛起的狀態(tài),而我們具體不知道瀏覽器的機(jī)制細(xì)節(jié)也不清楚,一般的網(wǎng)頁沒有關(guān)系,但大量的復(fù)雜的這種pending狀態(tài)勢(shì)必會(huì)導(dǎo)致內(nèi)存泄漏,具體的沒有測(cè)試過,后續(xù)可能會(huì)跟進(jìn)測(cè)試(nodeJS或webapp里面不推薦這樣),而我通過查詢也難以找到答案,這篇文章可以推薦看一下:從如何停掉 Promise 鏈說起??赡軐?duì)你有幫助在此種情況下如何做。
當(dāng)然一般情況下是不會(huì)存在泄漏,只是有這種風(fēng)險(xiǎn),無法取消Promise一直是它的痛點(diǎn)。而上述兩個(gè)奇妙的取消方法要具體情形具體使用。
2.8 Promise.all其實(shí)這幾個(gè)方法就簡(jiǎn)單了,就是一個(gè)簡(jiǎn)寫串聯(lián)所有你需要的Promise執(zhí)行,具體可以參照阮一峰的ES6Promise.all教程。
我這上一個(gè)代碼例子
Promise.all([setDelay(1000), setDelaySecond(1)]).then(result=>{ console.log(result); }) .catch(err=>{ console.log(err); }) // 輸出["我延遲了1000毫秒后輸出的", "我延遲了1秒后輸出的,注意單位是秒"]
輸出的是一個(gè)數(shù)組,相當(dāng)于把all方法里面的Promise并行執(zhí)行,注意是并行。
相當(dāng)于兩個(gè)Promise同時(shí)開始執(zhí)行,同時(shí)返回值,并不是先執(zhí)行第一個(gè)再執(zhí)行第二個(gè),如果你想串行執(zhí)行,請(qǐng)參考我后面寫的循環(huán)Promise循環(huán)串行(第4.2小節(jié))。
然后把resolve的值保存在數(shù)組中輸出。類似的還有Promise.race這里就不多贅述了。
三、Async/await介紹 3.1 基于Promise的Async/await什么是async/await呢?可以總結(jié)為一句話:async/await是一對(duì)好基友,缺一不可,他們的出生是為Promise服務(wù)的??梢哉fasync/await是Promise的爸爸,進(jìn)化版。為什么這么說呢?且聽我細(xì)細(xì)道來。
為什么要有async/await存在呢?
前文已經(jīng)說過了,為了解決大量復(fù)雜不易讀的Promise異步的問題,才出現(xiàn)的改良版。
這兩個(gè)基友必須同時(shí)出現(xiàn),缺一不可,那么先說一下Async:
async function process() { }
上面可以看出,async必須聲明的是一個(gè)function,不要去聲明別的,要是那樣await就不理你了(報(bào)錯(cuò))。
這樣聲明也是錯(cuò)的!
const async demo = function () {} // 錯(cuò)誤
必須緊跟著function。接下來說一下它的兄弟await。
上面說到必須是個(gè)函數(shù)(function),那么await就必須是在這個(gè)async聲明的函數(shù)內(nèi)部使用,否則就會(huì)報(bào)錯(cuò)。
就算你這樣寫,也是錯(cuò)的。
let data = "data" demo = async function () { const test = function () { await data } }
必須是直系(作用域鏈不能隔代),這樣會(huì)報(bào)錯(cuò):Uncaught SyntaxError: await is only valid in async function。
講完了基本規(guī)范,我們接下去說一下他們的本質(zhì)。
3.2 async的本質(zhì)敲黑板?。?!很重要!async聲明的函數(shù)的返回本質(zhì)上是一個(gè)Promise。
什么意思呢?就是說你只要聲明了這個(gè)函數(shù)是async,那么內(nèi)部不管你怎么處理,它的返回肯定是個(gè)Promise。
看下列例子:
(async function () { return "我是Promise" })() // 返回是Promise //Promise?{: "我是Promise"}
你會(huì)發(fā)現(xiàn)返回是這個(gè):Promise?{
自動(dòng)解析成Promise.resolve("我是Promise");
等同于:
(async function () { return Promise.resolve("我是Promise"); })()
所以你想像一般function的返回那樣,拿到返回值,原來的思維要改改了!你可以這樣拿到返回值:
const demo = async function () { return Promise.resolve("我是Promise"); // 等同于 return "我是Promise" // 等同于 return new Promise((resolve,reject)=>{ resolve("我是Promise") }) } demo.then(result=>{ console.log(result) // 這里拿到返回值 })
上述三種寫法都行,要看注釋細(xì)節(jié)都寫在里面了?。∠駥?duì)待Promise一樣去對(duì)待async的返回值?。。?/strong>
好的接下去我們看await的干嘛用的.
3.3 await的本質(zhì)與例子await的本質(zhì)是可以提供等同于”同步效果“的等待異步返回能力的語法糖。
這一句咋一看很別扭,好的不急,我們從例子開始看:
const demo = async ()=>{ let result = await new Promise((resolve, reject) => { setTimeout(()=>{ resolve("我延遲了一秒") }, 1000) }); console.log("我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)”"); } // demo的返回當(dāng)做Promise demo().then(result=>{ console.log("輸出",result); })
await顧名思義就是等待一會(huì),只要await聲明的函數(shù)還沒有返回,那么下面的程序是不會(huì)去執(zhí)行的?。?!。這就是字面意義的等待一會(huì)(等待返回再去執(zhí)行)。
那么你到這測(cè)試一下,你會(huì)發(fā)現(xiàn)輸出是這個(gè):輸出 undefined。這是為什么呢?這也是我想強(qiáng)調(diào)的一個(gè)地方?。?!
你在demo函數(shù)里面都沒聲明返回,哪來的then?所以正確寫法是這樣:
const demo = async ()=>{ let result = await new Promise((resolve, reject) => { setTimeout(()=>{ resolve("我延遲了一秒") }, 1000) }); console.log("我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)”"); return result; } // demo的返回當(dāng)做Promise demo().then(result=>{ console.log("輸出",result); // 輸出 我延遲了一秒 })
我推薦的寫法是帶上then,規(guī)范一點(diǎn),當(dāng)然你沒有返回也是沒問題的,demo會(huì)照常執(zhí)行。下面這種寫法是不帶返回值的寫法:
const demo = async ()=>{ let result = await new Promise((resolve, reject) => { setTimeout(()=>{ resolve("我延遲了一秒") }, 1000) }); console.log("我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)”"); } demo();
所以可以發(fā)現(xiàn),只要你用await聲明的異步返回,是必須“等待”到有返回值的時(shí)候,代碼才繼續(xù)執(zhí)行下去。
那事實(shí)是這樣嗎?你可以跑一下這段代碼:
const demo = async ()=>{ let result = await setTimeout(()=>{ console.log("我延遲了一秒"); }, 1000) console.log("我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)”"); return result } demo().then(result=>{ console.log("輸出",result); })
你會(huì)發(fā)現(xiàn),輸出是這樣的:
我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)” 輸出 1 我延遲了一秒
奇怪,并沒有await???setTimeout是異步啊,問題在哪?問題就在于setTimeout這是個(gè)異步,但是不是Promise!起不到“等待一會(huì)”的作用。
所以更準(zhǔn)確的說法應(yīng)該是用await聲明的Promise異步返回,必須“等待”到有返回值的時(shí)候,代碼才繼續(xù)執(zhí)行下去。
請(qǐng)記住await是在等待一個(gè)Promise的異步返回當(dāng)然這種等待的效果只存在于“異步”的情況,await可以用于聲明一般情況下的傳值嗎?
事實(shí)是當(dāng)然可以:
const demo = async ()=>{ let message = "我是聲明值" let result = await message; console.log(result); console.log("我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)”"); return result } demo().then(result=>{ console.log("輸出",result); })
輸出:
我是聲明值 我由于上面的程序還沒執(zhí)行完,先不執(zhí)行“等待一會(huì)” 輸出 我是聲明值
這里只要注意一點(diǎn):then的執(zhí)行總是最后的。
3.4 async/await 優(yōu)勢(shì)實(shí)戰(zhàn)現(xiàn)在我們看一下實(shí)戰(zhàn):
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != "number") reject(new Error("參數(shù)必須是number類型")); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } const setDelaySecond = (seconds) => { return new Promise((resolve, reject)=>{ if (typeof seconds != "number" || seconds > 10) reject(new Error("參數(shù)必須是number類型,并且小于等于10")); setTimeout(()=> { resolve(`我延遲了${seconds}秒后輸出的,注意單位是秒`) }, seconds * 1000) }) }
比如上面兩個(gè)延時(shí)函數(shù)(寫在上面),比如我想先延時(shí)1秒,在延遲2秒,再延時(shí)1秒,最后輸出“完成”,這個(gè)過程,如果用then的寫法,大概是這樣(嵌套地獄寫法出門右拐不送):
setDelay(1000) .then(result=>{ console.log(result); return setDelaySecond(2) }) .then(result=>{ console.log(result); return setDelay(1000) }) .then(result=>{ console.log(result); console.log("完成") }) .catch(err=>{ console.log(err); })
咋一看是不是挺繁瑣的?如果邏輯多了估計(jì)看得更累,現(xiàn)在我們來試一下async/await
(async ()=>{ const result = await setDelay(1000); console.log(result); console.log(await setDelaySecond(2)); console.log(await setDelay(1000)); console.log("完成了"); })()
看!是不是沒有冗余的長(zhǎng)長(zhǎng)的鏈?zhǔn)酱a,語義化也非常清楚,非常舒服,那么你看到這里,一定還發(fā)現(xiàn)了,上面的catch我們是不是沒有在async中實(shí)現(xiàn)?接下去我們就分析一下async/await如何處理錯(cuò)誤?
3.5 async/await錯(cuò)誤處理因?yàn)閍sync函數(shù)返回的是一個(gè)Promise,所以我們可以在外面catch住錯(cuò)誤。
const demo = async ()=>{ const result = await setDelay(1000); console.log(result); console.log(await setDelaySecond(2)); console.log(await setDelay(1000)); console.log("完成了"); } demo().catch(err=>{ console.log(err); })
在async函數(shù)的catch中捕獲錯(cuò)誤,當(dāng)做一個(gè)Pormise處理,同時(shí)你不想用這種方法,可以使用try...catch語句:
(async ()=>{ try{ const result = await setDelay(1000); console.log(result); console.log(await setDelaySecond(2)); console.log(await setDelay(1000)); console.log("完成了"); } catch (e) { console.log(e); // 這里捕獲錯(cuò)誤 } })()
當(dāng)然這時(shí)候你就不需要在外面catch了。
通常我們的try...catch數(shù)量不會(huì)太多,幾個(gè)最多了,如果太多了,說明你的代碼肯定需要重構(gòu)了,一定沒有寫得非常好。還有一點(diǎn)就是try...catch通常只用在需要的時(shí)候,有時(shí)候不需要catch錯(cuò)誤的地方就可以不寫。
有人會(huì)問了,我try...catch好像只能包裹代碼塊,如果我需要拆分開分別處理,不想因?yàn)橐粋€(gè)的錯(cuò)誤就整個(gè)process都crash掉了,那么難道我要寫一堆try...catch嗎?我就是別扭,我就是不想寫try...catch怎嘛辦?下面有一種很好的解決方案,僅供參考:
我們知道await后面跟著的肯定是一個(gè)Promise那是不是可以這樣寫?
(async ()=>{ const result = await setDelay(1000).catch(err=>{ console.log(err) }); console.log(result); const result1 = await setDelaySecond(12).catch(err=>{ console.log(err) }) console.log(result1); console.log(await setDelay(1000)); console.log("完成了"); })()
這樣輸出:
我延遲了1000毫秒后輸出的 Error: 參數(shù)必須是number類型,并且小于等于10 at Promise (test4.html:19) at new Promise () at setDelaySecond (test4.html:18) at test4.html:56 undefined 我延遲了1000毫秒后輸出的 完成了
是不是就算有錯(cuò)誤,也不會(huì)影響后續(xù)的操作,是不是很棒?當(dāng)然不是,你說這代碼也忒丑了吧,亂七八糟的,寫得別扭await又跟著catch。那么我們可以改進(jìn)一下,封裝一下提取錯(cuò)誤的代碼函數(shù):
// to function function to(promise) { return promise.then(data => { return [null, data]; }) .catch(err => [err]); // es6的返回寫法 }
返回的是一個(gè)數(shù)組,第一個(gè)是錯(cuò)誤,第二個(gè)是異步結(jié)果,使用如下:
(async ()=>{ // es6的寫法,返回一個(gè)數(shù)組(你可以改回es5的寫法覺得不習(xí)慣的話),第一個(gè)是錯(cuò)誤信息,第二個(gè)是then的異步返回?cái)?shù)據(jù),這里要注意一下重復(fù)變量聲明可能導(dǎo)致問題(這里舉例是全局,如果用let,const,請(qǐng)換變量名)。 [err, result] = await to(setDelay(1000)) // 如果err存在就是有錯(cuò),不想繼續(xù)執(zhí)行就拋出錯(cuò)誤 if (err) throw new Error("出現(xiàn)錯(cuò)誤,同時(shí)我不想執(zhí)行了"); console.log(result); [err, result1] = await to(setDelaySecond(12)) // 還想執(zhí)行就不要拋出錯(cuò)誤 if (err) console.log("出現(xiàn)錯(cuò)誤,同時(shí)我想繼續(xù)執(zhí)行", err); console.log(result1); console.log(await setDelay(1000)); console.log("完成了"); })()3.6 async/await的中斷(終止程序)
首先我們要明確的是,Promise本身是無法中止的,Promise本身只是一個(gè)狀態(tài)機(jī),存儲(chǔ)三個(gè)狀態(tài)(pending,resolved,rejected),一旦發(fā)出請(qǐng)求了,必須閉環(huán),無法取消,之前處于pending狀態(tài)只是一個(gè)掛起請(qǐng)求的狀態(tài),并不是取消,一般不會(huì)讓這種情況發(fā)生,只是用來臨時(shí)中止鏈?zhǔn)降倪M(jìn)行。
中斷(終止)的本質(zhì)在鏈?zhǔn)街兄皇菕炱?,并不是本質(zhì)的取消Promise請(qǐng)求,那樣是做不到的,Promise也沒有cancel的狀態(tài)。
不同于Promise的鏈?zhǔn)綄懛?,寫在async/await中想要中斷程序就很簡(jiǎn)單了,因?yàn)檎Z義化非常明顯,其實(shí)就和一般的function寫法一樣,想要中斷的時(shí)候,直接return一個(gè)值就行,null,空,false都是可以的。看例子:
let count = 6; const demo = async ()=>{ const result = await setDelay(1000); console.log(result); const result1 = await setDelaySecond(count); console.log(result1); if (count > 5) { return "我退出了,下面的不進(jìn)行了"; // return; // return false; // 這些寫法都可以 // return null; } console.log(await setDelay(1000)); console.log("完成了"); }; demo().then(result=>{ console.log(result); }) .catch(err=>{ console.log(err); })
實(shí)質(zhì)就是直接return返回了一個(gè)Promise,相當(dāng)于return Promise.resolve("我退出了下面不進(jìn)行了"),當(dāng)然你也可以返回一個(gè)“拒絕”:return Promise.reject(new Error("拒絕"))那么就會(huì)進(jìn)到錯(cuò)誤信息里去。
我們經(jīng)常會(huì)使用上述兩種寫法,也可能混用,有時(shí)候會(huì)遇到一些情況,這邊舉例子說明:
4.1 Promise獲取數(shù)據(jù)(串行)之then寫法注意并行的不用多說,很簡(jiǎn)單,直接循環(huán)發(fā)出請(qǐng)求就可以或者用Promise.all。如果我們需要串行循環(huán)一個(gè)請(qǐng)求,那么應(yīng)該怎么做呢?
我們需要實(shí)現(xiàn)一個(gè)依次分別延遲1秒輸出值,一共5秒的程序,首先是Promise的循環(huán),這個(gè)循環(huán)就相對(duì)來說比較麻煩:
先不說循環(huán),我們先舉一個(gè)錯(cuò)誤的例子,現(xiàn)在有一個(gè)延遲函數(shù)
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != "number") reject(new Error("參數(shù)必須是number類型")); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) }
我們想做到:“循環(huán)串行執(zhí)行延遲一秒的Promise函數(shù)”,期望的結(jié)果應(yīng)該是:隔一秒輸出我延遲了1000毫秒后輸出的,一共經(jīng)過循環(huán)3次。我們想當(dāng)然地寫出下列的鏈?zhǔn)綄懛ǎ?/p>
arr = [setDelay(1000), setDelay(1000), setDelay(1000)] arr[0] .then(result=>{ console.log(result) return arr[1] }) .then(result=>{ console.log(result) return arr[2] }) .then(result=>{ console.log(result) })
但是很不幸,你發(fā)現(xiàn)輸出是并行的?。?!也就是說一秒鐘一次性輸出了3個(gè)值!。那么這是什么情況呢?其實(shí)很簡(jiǎn)單。。。就是你把setDelay(1000)這個(gè)直接添加到數(shù)組的時(shí)候,其實(shí)就已經(jīng)執(zhí)行了,注意你的執(zhí)行語句(1000)
這其實(shí)是基礎(chǔ),是語言的特性,很多粗心的人(或者是沒有好好學(xué)習(xí)JS的人)會(huì)以為這樣就把函數(shù)添加到數(shù)組里面了,殊不知函數(shù)已經(jīng)執(zhí)行過一次了。
那么這樣導(dǎo)致的后果是什么呢?也就是說數(shù)組里面保存的每個(gè)Promise狀態(tài)都是resolve完成的狀態(tài)了,那么你后面鏈?zhǔn)秸{(diào)用直接return arr[1]其實(shí)沒有去請(qǐng)求,只是立即返回了一個(gè)resolve的狀態(tài)。所以你會(huì)發(fā)現(xiàn)程序是相當(dāng)于并行的,沒有依次順序調(diào)用。
那么解決方案是什么呢?直接函數(shù)名存儲(chǔ)函數(shù)的方式(不執(zhí)行Promise)來達(dá)到目的
我們這樣改一下程序:
arr = [setDelay, setDelay, setDelay] arr[0](1000) .then(result=>{ console.log(result) return arr[1](1000) }) .then(result=>{ console.log(result) return arr[2](1000) }) .then(result=>{ console.log(result) })
上述相當(dāng)于把Promise預(yù)先存儲(chǔ)在一個(gè)數(shù)組中,在你需要調(diào)用的時(shí)候,再去執(zhí)行。當(dāng)然你也可以用閉包的方式存儲(chǔ)起來,需要調(diào)用的時(shí)候再執(zhí)行。
4.2 Promise循環(huán)獲取數(shù)據(jù)(串行)之for循環(huán)上述寫法是不優(yōu)雅的,次數(shù)一多就GG了,為什么要提一下上面的then,其實(shí)就是為了后面的for循環(huán)做鋪墊。
上面的程序根據(jù)規(guī)律改寫一下:
arr = [setDelay, setDelay, setDelay] var temp temp = arr[0](1000) for (let i = 1; i <= arr.length; i++) { if (i == arr.length) { temp.then(result=>{ console.log("完成了"); }) break; } temp = temp.then((result)=>{ console.log(result); return arr[i-1](1000) }); }
錯(cuò)誤處理可以在for循環(huán)中套入try...catch,或者在你每個(gè)循環(huán)點(diǎn)進(jìn)行.then().catch()、都是可行的。如果你想提取成公共方法,可以再改寫一下,利用遞歸的方式:
首先你需要閉包你的Promise程序
function timeout(millisecond) { return ()=> { return setDelay(millisecond); } }
如果不閉包會(huì)導(dǎo)致什么后果呢?不閉包的話,你傳入的參數(shù)值后,你的Promise會(huì)馬上執(zhí)行,導(dǎo)致狀態(tài)改變,如果用閉包實(shí)現(xiàn)的話,你的Promise會(huì)一直保存著,等到你需要調(diào)用的時(shí)候再使用。而且最大的優(yōu)點(diǎn)是可以預(yù)先傳入你需要的參數(shù)。
改寫數(shù)組:
arr = [timeout(2000), timeout(1000), timeout(1000)]
提取方法,Promise數(shù)組作為參數(shù)傳入:
const syncPromise = function (arr) { const _syncLoop = function (count) { if (count === arr.length - 1) { // 是最后一個(gè)就直接return return arr[count]() } return arr[count]().then((result)=>{ console.log(result); return _syncLoop(count+1) // 遞歸調(diào)用數(shù)組下標(biāo) }); } return _syncLoop(0); }
使用:
syncPromise(arr).then(result=>{ console.log(result); console.log("完成了"); }) // 或者 添加到Promise類中方法 Promise.syncAll = function syncAll(){ return syncPromise }// 以后可以直接使用 Promise.syncAll(arr).then(result=>{ console.log(result); console.log("完成了"); })
還有大神總結(jié)了一個(gè)reduce的寫法,其實(shí)就是一個(gè)迭代數(shù)組的過程:
const p = arr.reduce((total, current)=>{ return total.then((result)=>{ console.log(result); return current() }) }, Promise.resolve("程序開始")) p.then((result)=>{ console.log("結(jié)束了", result); })
都是可行的,在Promise的循環(huán)領(lǐng)域。
4.3 async/await循環(huán)獲取數(shù)據(jù)(串行)之for循環(huán)現(xiàn)在就來介紹一下牛逼的async/await實(shí)戰(zhàn),上述的代碼你是不是要看吐了,的確,我也覺得好麻煩啊,那么如果用async/await能有什么改進(jìn)嗎?這就是它出現(xiàn)的意義:
模擬上述代碼的循環(huán):
(async ()=>{ arr = [timeout(2000), timeout(1000), timeout(1000)] for (var i=0; i < arr.length; i++) { result = await arr[i](); console.log(result); } })()
。。。這就完了?是的。。。就完了,是不是特別方便?。。?!語義化也非常明顯!!這里為了保持與上面風(fēng)格一致,沒有加入錯(cuò)誤處理,所以實(shí)戰(zhàn)的時(shí)候記得加入你的try...catch語句來捕獲錯(cuò)誤。
四、后記一直想總結(jié)一下Promise和async/await,很多地方可能總結(jié)得不夠,已經(jīng)盡力擴(kuò)大篇幅了,后續(xù)有新的知識(shí)點(diǎn)和總結(jié)點(diǎn)可能會(huì)更新(未完待續(xù)),但是入門這個(gè)基本夠用了。
我們常說什么async/await的出現(xiàn)淘汰了Promise,可以說是大錯(cuò)特錯(cuò),恰恰相反,正因?yàn)橛辛薖romise,才有了改良版的async/await,從上面分析就可以看出,兩者是相輔相成的,缺一不可。
想學(xué)好async/await必須先精通Promise,兩者密不可分,有不同意見和改進(jìn)的歡迎指導(dǎo)!
前端小白,大家互相交流,peace!
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://www.ezyhdfw.cn/yun/98651.html
摘要:前言一直混跡社區(qū)突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點(diǎn)混亂所以將前端主流技術(shù)做了一個(gè)書簽整理不求最多最全但求最實(shí)用。 前言 一直混跡社區(qū),突然發(fā)現(xiàn)自己收藏了不少好文但是管理起來有點(diǎn)混亂; 所以將前端主流技術(shù)做了一個(gè)書簽整理,不求最多最全,但求最實(shí)用。 書簽源碼 書簽導(dǎo)入瀏覽器效果截圖showImg(https://segmentfault.com/img/bVbg41b?w=107...
摘要:異步函數(shù)是值通過事件循環(huán)異步執(zhí)行的函數(shù),它會(huì)通過一個(gè)隱式的返回其結(jié)果。 async 異步函數(shù) 不完全使用攻略 前言 現(xiàn)在已經(jīng)到 8012 年的尾聲了,前端各方面的技術(shù)發(fā)展也層出不窮,VueConf TO 2018 大會(huì) 也發(fā)布了 Vue 3.0的計(jì)劃。而在我們(我)的日常中也經(jīng)常用 Vue 來編寫一些項(xiàng)目。那么,就少不了 ES6 的登場(chǎng)了。那么話說回來,你真的會(huì)用 ES6 的 asyn...
摘要:的翻譯文檔由的維護(hù)很多人說,阮老師已經(jīng)有一本關(guān)于的書了入門,覺得看看這本書就足夠了。前端的異步解決方案之和異步編程模式在前端開發(fā)過程中,顯得越來越重要。為了讓編程更美好,我們就需要引入來降低異步編程的復(fù)雜性。 JavaScript Promise 迷你書(中文版) 超詳細(xì)介紹promise的gitbook,看完再不會(huì)promise...... 本書的目的是以目前還在制定中的ECMASc...
摘要:想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳博客一年百來篇優(yōu)質(zhì)文章等著你引入的在的異步編程中是一個(gè)極好的改進(jìn)。可能會(huì)產(chǎn)生誤導(dǎo)一些文章將與進(jìn)行了比較,并聲稱它是下一代異步編程風(fēng)格,對(duì)此作者深表異議。結(jié)論引入的關(guān)鍵字無疑是對(duì)異步編程的改進(jìn)。 showImg(https://segmentfault.com/img/bVbjFP0?w=800&h=450); 想閱讀更多優(yōu)質(zhì)文章請(qǐng)猛戳GitHub博客,一年百來篇...
摘要:解決辦法,將箭頭函數(shù)聲明為函數(shù),代碼如下運(yùn)行結(jié)果至此,問題解決。必須在函數(shù)的上下文中。對(duì)程序而言有了上下文調(diào)用幀才有一個(gè)完整的邏輯過程。 先簡(jiǎn)單介紹下async await: async/await是ES6推出的異步處理方案,目的也很明確:更好的實(shí)現(xiàn)異步編程。 詳細(xì)見阮大神 ES6入門 現(xiàn)在說說實(shí)踐中遇到的問題:使用await報(bào)錯(cuò)Unexpected identifier 先...
閱讀 1599·2023-04-25 17:41
閱讀 3107·2021-11-22 15:08
閱讀 911·2021-09-29 09:35
閱讀 1677·2021-09-27 13:35
閱讀 3401·2021-08-31 09:44
閱讀 2773·2019-08-30 13:20
閱讀 2009·2019-08-30 13:00
閱讀 2622·2019-08-26 12:12