非同步Promise及Async/Await可能最完整入門攻略
此文只介紹Async/Await與Promise基礎知識與實際用到注意的問題,將通過很多程式碼例項進行說明,兩個例項程式碼是setDelay
和setDelaySecond
。
tips:本文系原創轉自我的部落格非同步Promise及Async/Await最完整入門攻略,歡迎前端大神交流,指出問題
一、為什麼有Async/Await?
我們都知道已經有了Promise
的解決方案了,為什麼還要ES7提出新的Async/Await標準呢?
答案其實也顯而易見:Promise
雖然跳出了非同步巢狀的怪圈,用鏈式表達更加清晰,但是我們也發現如果有大量的非同步請求的時候,流程複雜的情況下,會發現充滿了螢幕的then
首先,我們必須瞭解Promise
。
二、Promise簡介
2.1 Promise例項
什麼是Promise,很多人應該都知道基礎概念?直接看下面的程式碼(全文的例子都是基於setDelaySecond
和setDelay
兩個函式,請務必記住):
const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('引數必須是number型別')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒後輸出的`) }, millisecond) }) }
我們把一個Promise封裝在一個函式裡面同時返回了一個Promise,這樣比較規範。
可以看到定義的Promise有兩個引數,resolve
和reject
。
-
resolve
:將非同步的執行從pending(請求)
變成了resolve(成功返回)
,是個函式執行返回。 -
reject
:顧名思義“拒絕”,就是從請求變成了"失敗",是個函式可以執行返回一個結果,但我們這裡推薦大家返回一個錯誤new Error()
。
上述例子,你可以
reject('返回一個字串')
,隨便你返回,但是我們還是
建議返回一個Error物件,這樣更加清晰是“失敗的”,這樣更規範一點。
2.2 Promise的then和catch
我們通過Promise的原型方法then
拿到我們的返回值:
setDelay(3000)
.then((result)=>{
console.log(result) // 輸出“我延遲了2000毫秒後輸出的”
})
輸出下列的值:“我延遲了2000毫秒後輸出的”。
如果出錯呢?那就用catch
捕獲:
setDelay('我是字串')
.then((result)=>{
console.log(result) // 不進去了
})
.catch((err)=>{
console.log(err) // 輸出錯誤:“引數必須是number型別”
})
是不是很簡單?好,現在我增加一點難度,如果多個Promise
執行會是怎麼樣呢?
2.3 Promise相互依賴
我們在寫一個Promise:
const setDelaySecond = (seconds) => {
return new Promise((resolve, reject)=>{
if (typeof seconds != 'number' || seconds > 10) reject(new Error('引數必須是number型別,並且小於等於10'));
setTimeout(()=> {
console.log(`先是setDelaySeconds函式輸出,延遲了${seconds}秒,一共需要延遲${seconds+2}秒`)
resolve(setDelay(2000)) // 這裡依賴上一個Promise
}, seconds * 1000)
})
}
在下一個需要依賴的resolve
去返回另一個Promise,會發生什麼呢?我們執行一下:
setDelaySecond(3).then((result)=>{
console.log(result)
}).catch((err)=>{
console.log(err);
})
你會發現結果是先執行:“先是setDelaySeconds輸出,延遲了2秒,一共需要延遲5秒”
再執行setDelay
的resolve
:“我延遲了2000毫秒後輸出的”。的確做到了依次執行的目的。
有人說,我不想耦合性這麼高,想先執行setDelay
函式再執行setDelaySecond
,但不想用上面那種寫法,可以嗎,答案是當然可以。
2.4 Promise鏈式寫法
先改寫一下setDelaySecond
,拒絕依賴,降低耦合性
const setDelaySecond = (seconds) => {
return new Promise((resolve, reject)=>{
if (typeof seconds != 'number' || seconds > 10) reject(new Error('引數必須是number型別,並且小於等於10'));
setTimeout(()=> {
resolve(`我延遲了${seconds}秒後輸出的,是第二個函式`)
}, seconds * 1000)
})
}
先執行setDelay
在執行setDelaySecond
,只需要在第一個then
的結果中返回下一個Promise就可以一直鏈式寫下去了,相當於依次執行:
setDelay(2000)
.then((result)=>{
console.log(result)
console.log('我進行到第一步的');
return setDelaySecond(3)
})
.then((result)=>{
console.log('我進行到第二步的');
console.log(result);
}).catch((err)=>{
console.log(err);
})
發現確實達到了可喜的鏈式(終於脫離非同步巢狀苦海,哭),可以看到then
的鏈式寫法非常優美。
2.5 鏈式寫法需要注意的地方
這裡一定要提到一點:
then
式鏈式寫法的本質其實是一直往下傳遞返回一個新的Promise,也就是說then在下一步接收的是上一步返回的Promise,理解這個對於後面的細節非常重要!!
那麼並不是這麼簡單,then的返回我們可以看出有2個引數(都是回撥):
- 第一個回撥是resolve的回撥,也就是第一個引數用得最多,拿到的是上一步的
Promise
成功resolve
的值。 - 第二個回撥是reject的回撥,用的不多,但是求求大家不要寫錯了,通常是拿到上一個的錯誤,那麼這個錯誤處理和catch有什麼區別和需要注意的地方呢?
我們修改上面的程式碼:
setDelay(2000)
.then((result)=>{
console.log(result)
console.log('我進行到第一步的');
return setDelaySecond(20)
})
.then((result)=>{
console.log('我進行到第二步的');
console.log(result);
}, (_err)=> {
console.log('我出錯啦,進到這裡捕獲錯誤,但是不經過catch了');
})
.then((result)=>{
console.log('我還是繼續執行的!!!!')
})
.catch((err)=>{
console.log(err);
})
可以看到輸出結果是:進到了then
的第二個引數(reject)中去了,而且最重要的是!不再經過catch
了。
那麼我們把catch挪上去,寫到then
錯誤處理前:
setDelay(2000)
.then((result)=>{
console.log(result)
console.log('我進行到第一步的');
return setDelaySecond(20)
})
.catch((err)=>{ // 挪上去了
console.log(err); // 這裡catch到上一個返回Promise的錯誤
})
.then((result)=>{
console.log('我進行到第二步的');
console.log(result);
}, (_err)=> {
console.log('我出錯啦,但是由於catch在我前面,所以錯誤早就被捕獲了,我這沒有錯誤了');
})
.then((result)=>{
console.log('我還是繼續執行的!!!!')
})
可以看到先經過catch
的捕獲,後面就沒錯誤了。
可以得出需要注意的:
-
catch
寫法是針對於整個鏈式寫法的錯誤捕獲的,而then
第二個引數是針對於上一個返回Promise
的。 - 兩者的優先順序:就是看誰在鏈式寫法的前面,在前面的先捕獲到錯誤,後面就沒有錯誤可以捕獲了,鏈式前面的優先順序大,而且兩者都不是
break
, 可以繼續執行後續操作不受影響。
2.5 鏈式寫法的錯誤處理
上述已經寫好了關於then裡面三個回撥中第二個回撥(reject)會與catch衝突的問題,那麼我們實際寫的時候,引數捕獲的方式基本寫得少,catch的寫法會用到更多。
既然有了很多的Promise,那麼我需不需要寫很多catch呢?
答案當然是:不需要!,哪有那麼麻煩的寫法,只需要在末尾catch
一下就可以了,因為鏈式寫法的錯誤處理具有“冒泡”特性,鏈式中任何一個環節出問題,都會被catch
到,同時在某個環節後面的程式碼就不會執行了。
既然說到這裡,我們把catch
移到第一個鏈式的返回裡面會發生什麼事呢?看下面程式碼:
setDelay('2000')
.then((result)=>{
console.log('第一步完成了');
console.log(result)
return setDelaySecond(3)
})
.catch((err)=>{ // 這裡移到第一個鏈式去,發現上面的不執行了,下面的繼續執行
console.log(err);
})
.then((result)=>{
console.log('第二步完成了');
console.log(result);
})
驚喜的發現,鏈式繼續走下去了!!輸出如下(undefined是因為上一個then沒有返回一個Promise):
重點來了!敲黑板!!鏈式中的catch
並不是終點!!catch完如果還有then還會繼續往下走!不信的話可以把第一個catch
在最後面的那個例子後面再加幾個then
,你會發現並不會跳出鏈式執行。
如果順序執行setDelay,setDelay1,setDelaySecond
,按照上述的邏輯,流程圖可以概括如下:
catch
只是捕獲錯誤的一個鏈式表達,並不是break!
所以,catch放的位置也很有講究,一般放在一些重要的、必須catch的程式的最後。**這些重要的程式中間一旦出現錯誤,會馬上跳過其他後續程式的操作直接執行到最近的catch程式碼塊,但不影響catch後續的操作!!!!
到這就不得不體一個ES2018標準新引入的Promise的finally
,表示在catch後必須肯定會預設執行的的操作。這裡不多展開,細節可以參考:Promise的finally
2.5 Promise鏈式中間想返回自定義的值
其實很簡單,用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('這裡可以繼續返回')
})
.catch((err)=>{
console.log(err);
})
2.7 如何跳出或停止Promise鏈式
不同於一般的function
的break
的方式,如果你是這樣的操作:func().then().then().then().catch()
的方式,你想在第一個then
就跳出鏈式,後面的不想執行了,不同於一般的break;return null;return false
等操作,可以說,如何停止Promise鏈,是一大難點,是整個Promise最複雜的地方。
1.用鏈式的思維想,我們拒絕掉某一鏈,那麼不就是相當於直接跳到了catch模組嗎?
我們是不是可以直接“拒絕“掉達到停止的目的?
setDelay(2000)
.then((result)=>{
console.log(result)
console.log('我進行到第一步的');
return setDelaySecond(1)
})
.then((result)=>{
console.log('我進行到第二步的');
console.log(result);
console.log('我主動跳出迴圈了');
return Promise.reject('跳出迴圈的資訊') // 這裡返回一個reject,主動跳出迴圈了
})
.then((result)=>{
console.log('我不執行');
})
.catch((mes)=>{
console.dir(mes)
console.log('我跳出了');
})
但是很容易看到缺點:有時候你並不確定是因為錯誤跳出的,還是主動跳出的,所以我們可以加一個標誌位:
return Promise.reject({
isNotErrorExpection: true // 返回的地方加一個標誌位,判斷是否是錯誤型別,如果不是,那麼說明可以是主動跳出迴圈的
})
或者根據上述的程式碼判斷catch的地方輸出的型別是不是屬於錯誤物件的,是的話說明是錯誤,不是的話說明是主動跳出的,你可以自己選擇(這就是為什麼要統一錯誤reject的時候輸出new Error('錯誤資訊')的原因,規範!)
當然你也可以直接丟擲一個錯誤跳出:
throw new Error('錯誤資訊') // 直接跳出,那就不能用判斷是否為錯誤物件的方法進行判斷了
2.那有時候我們有這個需求:catch是放在中間(不是末尾),而同時我們又不想執行catch後面的程式碼,也就是鏈式的絕對中止,應該怎麼辦?
我們看這段程式碼:
setDelay(2000)
.then((result)=>{
console.log(result)
console.log('我進行到第一步的');
return setDelaySecond(1)
})
.then((result)=>{
console.log('我進行到第二步的');
console.log(result);
console.log('我主動跳出迴圈了');
return Promise.reject('跳出迴圈的資訊') // 這裡直接呼叫Promise原型方法返回一個reject,主動跳出迴圈了
})
.then((result)=>{
console.log('我不執行');
})
.catch((mes)=>{
console.dir(mes)
console.log('我跳出了');
})
.then((res)=>{
console.log('我不想執行,但是卻執行了'); // 問題在這,上述的終止方法治標不治本。
})
這時候最後一步then
還是執行了,整條鏈都其實沒有本質上的跳出,那應該怎麼辦呢?
敲黑板!!重點來了!我們看Promise/A+規範可以知道:
A promise must be in one of three states: pending, fulfilled, or rejected.
Promise其實是有三種狀態的:pending,resolve,rejected,那麼我們一直在討論resolve和rejected
這2個狀態,是不是忽視了pending
這個狀態呢?pending狀態顧名思義就是請求中的狀態,成功請求就是resolve,失敗就是reject,其實他就是個中間過渡狀態。
而我們上面討論過了,then
的下一層級其實得到的是上一層級返回的Promise物件,也就是說原Promise物件與新物件狀態保持一致。那麼重點來了,如果你想在這一層級進行終止,是不是直接讓它永遠都pending
下去,那麼後續的操作不就沒了嗎?是不是就達到這個目的了??覺得有疑問的可以參考Promise/A+規範。
我們直接看程式碼:
setDelay(2000)
.then((result)=>{
console.log(result)
console.log('我進行到第一步的');
return setDelaySecond(1)
})
.then((result)=>{
console.log(result);
console.log('我主動跳出迴圈了');
// return Promise.reject('跳出迴圈的資訊')
// 重點在這
return new Promise(()=>{console.log('後續的不會執行')}) // 這裡返回的一個新的Promise,沒有resolve和reject,那麼會一直處於pending狀態,因為沒返回啊,那麼這種狀態就一直保持著,中斷了這個Promise
})
.then((result)=>{
console.log('我不執行');
})
.catch((mes)=>{
console.dir(mes)
console.log('我跳出了');
})
.then((res)=>{
console.log('我也不會執行')
})
這樣就解決了上述,錯誤跳出而導致無法完全終止Promise鏈的問題。
但是!隨之而來也有一個問題,那就是可能會導致潛在的記憶體洩漏,因為我們知道這個一直處於pending狀態下的Promise會一直處於被掛起的狀態,而我們具體不知道瀏覽器的機制細節也不清楚,一般的網頁沒有關係,但大量的複雜的這種pending狀態勢必會導致記憶體洩漏,具體的沒有測試過,後續可能會跟進測試(nodeJS或webapp裡面不推薦這樣),而我通過查詢也難以找到答案,這篇文章可以推薦看一下:從如何停掉 Promise 鏈說起。可能對你有幫助在此種情況下如何做。
當然一般情況下是不會存在洩漏,只是有這種風險,無法取消Promise一直是它的痛點。而上述兩個奇妙的取消方法要具體情形具體使用。
2.8 Promise.all
其實這幾個方法就簡單了,就是一個簡寫串聯所有你需要的Promise
執行,具體可以參照阮一峰的ES6Promise.all教程。
我這上一個程式碼例子
Promise.all([setDelay(1000), setDelaySecond(1)]).then(result=>{
console.log(result);
})
.catch(err=>{
console.log(err);
})
// 輸出["我延遲了1000毫秒後輸出的", "我延遲了1秒後輸出的,注意單位是秒"]
輸出的是一個數組,相當於把all
方法裡面的Promise
並行執行,注意是並行。
相當於兩個Promise同時開始執行,同時返回值,並不是先執行第一個再執行第二個,如果你想序列執行,請參考我後面寫的迴圈Promise迴圈序列(第4.2小節)。
然後把resolve的值儲存在陣列中輸出。類似的還有Promise.race這裡就不多贅述了。
三、Async/await介紹
3.1 基於Promise的Async/await
什麼是async/await
呢?可以總結為一句話:async/await是一對好基友,缺一不可,他們的出生是為Promise服務的。可以說async/await是Promise的爸爸,進化版。為什麼這麼說呢?且聽我細細道來。
為什麼要有async/await
存在呢?
前文已經說過了,為了解決大量複雜不易讀的Promise非同步的問題,才出現的改良版。
這兩個基友必須同時出現,缺一不可,那麼先說一下Async
:
async function process() {
}
上面可以看出,async
必須宣告的是一個function,不要去宣告別的,要是那樣await
就不理你了(報錯)。
這樣宣告也是錯的!
const async demo = function () {} // 錯誤
必須緊跟著function
。接下來說一下它的兄弟await
。
上面說到必須是個函式(function),那麼await
就必須是在這個async
宣告的函式內部使用,否則就會報錯。
就算你這樣寫,也是錯的。
let data = 'data'
demo = async function () {
const test = function () {
await data
}
}
必須是直系(作用域鏈不能隔代),這樣會報錯:Uncaught SyntaxError: await is only valid in async function
。
講完了基本規範,我們接下去說一下他們的本質。
3.2 async的本質
敲黑板!!!很重要!async宣告的函式的返回本質上是一個Promise。
什麼意思呢?就是說你只要聲明瞭這個函式是async
,那麼內部不管你怎麼處理,它的返回肯定是個Promise。
看下列例子:
(async function () {
return '我是Promise'
})()
// 返回是Promise
//Promise {<resolved>: "我是Promise"}
你會發現返回是這個:Promise {<resolved>: "我是Promise"}
。
自動解析成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) // 這裡拿到返回值
})
上述三種寫法都行,要看註釋細節都寫在裡面了!!像對待Promise一樣去對待async的返回值!!!
好的接下去我們看await
的幹嘛用的.
3.3 await的本質與例子
await的本質是可以提供等同於”同步效果“的等待非同步返回能力的語法糖。
這一句咋一看很彆扭,好的不急,我們從例子開始看:
const demo = async ()=>{
let result = await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('我延遲了一秒')
}, 1000)
});
console.log('我由於上面的程式還沒執行完,先不執行“等待一會”');
}
// demo的返回當做Promise
demo().then(result=>{
console.log('輸出',result);
})
await顧名思義就是等待一會,只要await
宣告的函式還沒有返回,那麼下面的程式是不會去執行的!!!。這就是字面意義的等待一會(等待返回再去執行)。
那麼你到這測試一下,你會發現輸出是這個:輸出 undefined
。這是為什麼呢?這也是我想強調的一個地方!!!
你在demo
函式裡面都沒宣告返回,哪來的then
?所以正確寫法是這樣:
const demo = async ()=>{
let result = await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('我延遲了一秒')
}, 1000)
});
console.log('我由於上面的程式還沒執行完,先不執行“等待一會”');
return result;
}
// demo的返回當做Promise
demo().then(result=>{
console.log('輸出',result); // 輸出 我延遲了一秒
})
我推薦的寫法是帶上then
,規範一點,當然你沒有返回也是沒問題的,demo
會照常執行。下面這種寫法是不帶返回值的寫法:
const demo = async ()=>{
let result = await new Promise((resolve, reject) => {
setTimeout(()=>{
resolve('我延遲了一秒')
}, 1000)
});
console.log('我由於上面的程式還沒執行完,先不執行“等待一會”');
}
demo();
所以可以發現,只要你用await宣告的非同步返回,是必須“等待”到有返回值的時候,程式碼才繼續執行下去。
那事實是這樣嗎?你可以跑一下這段程式碼:
const demo = async ()=>{
let result = await setTimeout(()=>{
console.log('我延遲了一秒');
}, 1000)
console.log('我由於上面的程式還沒執行完,先不執行“等待一會”');
return result
}
demo().then(result=>{
console.log('輸出',result);
})
你會發現,輸出是這樣的:
我由於上面的程式還沒執行完,先不執行“等待一會”
輸出 1
我延遲了一秒
奇怪,並沒有await啊?setTimeout
是非同步啊,問題在哪?問題就在於setTimeout
這是個非同步,但是不是Promise
!起不到“等待一會”的作用。
所以更準確的說法應該是用await宣告的Promise非同步返回,必須“等待”到有返回值的時候,程式碼才繼續執行下去。
請記住await是在等待一個Promise的非同步返回
當然這種等待的效果只存在於“非同步”的情況,await可以用於宣告一般情況下的傳值嗎?
事實是當然可以:
const demo = async ()=>{
let message = '我是宣告值'
let result = await message;
console.log(result);
console.log('我由於上面的程式還沒執行完,先不執行“等待一會”');
return result
}
demo().then(result=>{
console.log('輸出',result);
})
輸出:
我是宣告值
我由於上面的程式還沒執行完,先不執行“等待一會”
輸出 我是宣告值
這裡只要注意一點:then
的執行總是最後的。
3.4 async/await 優勢實戰
現在我們看一下實戰:
const setDelay = (millisecond) => {
return new Promise((resolve, reject)=>{
if (typeof millisecond != 'number') reject(new Error('引數必須是number型別'));
setTimeout(()=> {
resolve(`我延遲了${millisecond}毫秒後輸出的`)
}, millisecond)
})
}
const setDelaySecond = (seconds) => {
return new Promise((resolve, reject)=>{
if (typeof seconds != 'number' || seconds > 10) reject(new Error('引數必須是number型別,並且小於等於10'));
setTimeout(()=> {
resolve(`我延遲了${seconds}秒後輸出的,注意單位是秒`)
}, seconds * 1000)
})
}
比如上面兩個延時函式(寫在上面),比如我想先延時1秒,在延遲2秒,再延時1秒,最後輸出“完成”,這個過程,如果用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);
})
咋一看是不是挺繁瑣的?如果邏輯多了估計看得更累,現在我們來試一下async/await
(async ()=>{
const result = await setDelay(1000);
console.log(result);
console.log(await setDelaySecond(2));
console.log(await setDelay(1000));
console.log('完成了');
})()
看!是不是沒有冗餘的長長的鏈式程式碼,語義化也非常清楚,非常舒服,那麼你看到這裡,一定還發現了,上面的catch
我們是不是沒有在async中實現?接下去我們就分析一下async/await如何處理錯誤?
3.5 async/await錯誤處理
因為async函式返回的是一個Promise,所以我們可以在外面catch
住錯誤。
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函式的catch
中捕獲錯誤,當做一個Pormise處理,同時你不想用這種方法,可以使用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); // 這裡捕獲錯誤
}
})()
當然這時候你就不需要在外面catch
了。
通常我們的try...catch
數量不會太多,幾個最多了,如果太多了,說明你的程式碼肯定需要重構了,一定沒有寫得非常好。還有一點就是try...catch通常只用在需要的時候,有時候不需要catch錯誤的地方就可以不寫。
有人會問了,我try...catch
好像只能包裹程式碼塊,如果我需要拆分開分別處理,不想因為一個的錯誤就整個process都crash掉了,那麼難道我要寫一堆try...catch
嗎?我就是彆扭,我就是不想寫try...catch
怎嘛辦?下面有一種很好的解決方案,僅供參考:
我們知道await後面跟著的肯定是一個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: 引數必須是number型別,並且小於等於10
at Promise (test4.html:19)
at new Promise (<anonymous>)
at setDelaySecond (test4.html:18)
at test4.html:56
undefined
我延遲了1000毫秒後輸出的
完成了
是不是就算有錯誤,也不會影響後續的操作,是不是很棒?當然不是,你說這程式碼也忒醜了吧,亂七八糟的,寫得彆扭await又跟著catch。那麼我們可以改進一下,封裝一下提取錯誤的程式碼函式:
// to function
function to(promise) {
return promise.then(data => {
return [null, data];
})
.catch(err => [err]); // es6的返回寫法
}
返回的是一個數組,第一個是錯誤,第二個是非同步結果,使用如下:
(async ()=>{
// es6的寫法,返回一個數組(你可以改回es5的寫法覺得不習慣的話),第一個是錯誤資訊,第二個是then的非同步返回資料,這裡要注意一下重複變數宣告可能導致問題(這裡舉例是全域性,如果用let,const,請換變數名)。
[err, result] = await to(setDelay(1000))
// 如果err存在就是有錯,不想繼續執行就丟擲錯誤
if (err) throw new Error('出現錯誤,同時我不想執行了');
console.log(result);
[err, result1] = await to(setDelaySecond(12))
// 還想執行就不要丟擲錯誤
if (err) console.log('出現錯誤,同時我想繼續執行', err);
console.log(result1);
console.log(await setDelay(1000));
console.log('完成了');
})()
3.6 async/await的中斷(終止程式)
首先我們要明確的是,Promise
本身是無法中止的,Promise
本身只是一個狀態機,儲存三個狀態(pending,resolved,rejected),一旦發出請求了,必須閉環,無法取消,之前處於pending狀態只是一個掛起請求的狀態,並不是取消,一般不會讓這種情況發生,只是用來臨時中止鏈式的進行。
中斷(終止)的本質在鏈式中只是掛起,並不是本質的取消Promise
請求,那樣是做不到的,Promise
也沒有cancel
的狀態。
不同於Promise
的鏈式寫法,寫在async/await中想要中斷程式就很簡單了,因為語義化非常明顯,其實就和一般的function
寫法一樣,想要中斷的時候,直接return
一個值就行,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 '我退出了,下面的不進行了';
// return;
// return false; // 這些寫法都可以
// return null;
}
console.log(await setDelay(1000));
console.log('完成了');
};
demo().then(result=>{
console.log(result);
})
.catch(err=>{
console.log(err);
})
實質就是直接return
返回了一個Promise
,相當於return Promise.resolve('我退出了下面不進行了')
,當然你也可以返回一個“拒絕”:return Promise.reject(new Error('拒絕'))
那麼就會進到錯誤資訊裡去。
async函式實質就是返回一個Promise!
四、實戰中非同步需要注意的地方
我們經常會使用上述兩種寫法,也可能混用,有時候會遇到一些情況,這邊舉例子說明:
4.1 Promise獲取資料(序列)之then寫法注意
並行的不用多說,很簡單,直接迴圈發出請求就可以或者用Promise.all
。如果我們需要序列迴圈一個請求,那麼應該怎麼做呢?
我們需要實現一個依次分別延遲1秒輸出值,一共5秒的程式,首先是Promise的迴圈,這個迴圈就相對來說比較麻煩:
我們經常會犯的錯誤!就是不重視函式名與函式執行對程式的影響
先不說迴圈,我們先舉一個錯誤的例子,現在有一個延遲函式
const setDelay = (millisecond) => {
return new Promise((resolve, reject)=>{
if (typeof millisecond != 'number') reject(new Error('引數必須是number型別'));
setTimeout(()=> {
resolve(`我延遲了${millisecond}毫秒後輸出的`)
}, millisecond)
})
}
我們想做到:“迴圈序列執行延遲一秒的Promise函式”,期望的結果應該是:隔一秒輸出我延遲了1000毫秒後輸出的
,一共經過迴圈3次。我們想當然地寫出下列的鏈式寫法:
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)
})
但是很不幸,你發現輸出是並行的!!!也就是說一秒鐘一次性輸出了3個值!。那麼這是什麼情況呢?其實很簡單。。。就是你把setDelay(1000)
這個直接新增到陣列的時候,其實就已經執行了,注意你的執行語句(1000)
這其實是基礎,是語言的特性,很多粗心的人(或者是沒有好好學習JS的人)會以為這樣就把函式新增到數組裡面了,殊不知函式已經執行過一次了。
那麼這樣導致的後果是什麼呢?也就是說數組裡面儲存的每個Promise
狀態都是resolve
完成的狀態了,那麼你後面鏈式呼叫直接return arr[1]
其實沒有去請求,只是立即返回了一個resolve的狀態。所以你會發現程式是相當於並行的,沒有依次順序呼叫。
那麼解決方案是什麼呢?直接函式名儲存函式的方式(不執行Promise)來達到目的
我們這樣改一下程式:
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)
})
上述相當於把Promise
預先儲存在一個數組中,在你需要呼叫的時候,再去執行。當然你也可以用閉包的方式儲存起來,需要呼叫的時候再執行。
4.2 Promise迴圈獲取資料(序列)之for迴圈
上述寫法是不優雅的,次數一多就GG了,為什麼要提一下上面的then
,其實就是為了後面的for
迴圈做鋪墊。
上面的程式根據規律改寫一下:
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)
});
}
錯誤處理可以在for迴圈中套入try...catch
,或者在你每個迴圈點進行.then().catch()
、都是可行的。如果你想提取成公共方法,可以再改寫一下,利用遞迴的方式:
首先你需要閉包你的Promise
程式
function timeout(millisecond) {
return ()=> {
return setDelay(millisecond);
}
}
如果不閉包會導致什麼後果呢?不閉包的話,你傳入的引數值後,你的Promise會馬上執行,導致狀態改變,如果用閉包實現的話,你的Promise會一直儲存著,等到你需要呼叫的時候再使用。而且最大的優點是可以預先傳入你需要的引數。
改寫陣列:
arr = [timeout(2000), timeout(1000), timeout(1000)]
提取方法,Promise
陣列作為引數傳入:
const syncPromise = function (arr) {
const _syncLoop = function (count) {
if (count === arr.length - 1) { // 是最後一個就直接return
return arr[count]()
}
return arr[count]().then((result)=>{
console.log(result);
return _syncLoop(count+1) // 遞迴呼叫陣列下標
});
}
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('完成了');
})
還有大神總結了一個reduce
的寫法,其實就是一個迭代陣列的過程:
const p = arr.reduce((total, current)=>{
return total.then((result)=>{
console.log(result);
return current()
})
}, Promise.resolve('程式開始'))
p.then((result)=>{
console.log('結束了', result);
})
都是可行的,在Promise
的迴圈領域。
4.3 async/await迴圈獲取資料(序列)之for迴圈
現在就來介紹一下牛逼的async/await實戰,上述的程式碼你是不是要看吐了,的確,我也覺得好麻煩啊,那麼如果用async/await
能有什麼改進嗎?這就是它出現的意義:
模擬上述程式碼的迴圈:
(async ()=>{
arr = [timeout(2000), timeout(1000), timeout(1000)]
for (var i=0; i < arr.length; i++) {
result = await arr[i]();
console.log(result);
}
})()
。。。這就完了?是的。。。就完了,是不是特別方便!!!!語義化也非常明顯!!這裡為了保持與上面風格一致,沒有加入錯誤處理,所以實戰的時候記得加入你的try...catch
語句來捕獲錯誤。
四、後記
一直想總結一下Promise
和async/await
,很多地方可能總結得不夠,已經盡力擴大篇幅了,後續有新的知識點和總結點可能會更新(未完待續),但是入門這個基本夠用了。
我們常說什麼async/await
的出現淘汰了Promise,可以說是大錯特錯,恰恰相反,正因為有了Promise,才有了改良版的async/await
,從上面分析就可以看出,兩者是相輔相成的,缺一不可。
想學好async/await
必須先精通Promise
,兩者密不可分,有不同意見和改進的歡迎指導!
前端小白,大家互相交流,peace!