Promise及Async/Await


 

一、為什么有Async/Await?

我們都知道已經有了Promise的解決方案了,為什么還要ES7提出新的Async/Await標准呢?

答案其實也顯而易見:Promise雖然跳出了異步嵌套的怪圈,用鏈式表達更加清晰,但是我們也發現如果有大量的異步請求的時候,流程復雜的情況下,會發現充滿了屏幕的then,看起來非常吃力,而ES7的Async/Await的出現就是為了解決這種復雜的情況。

首先,我們必須了解Promise

二、Promise簡介

2.1 Promise實例

什么是Promise,很多人應該都知道基礎概念?直接看下面的代碼:

const setDelay = (millisecond) => { return new Promise((resolve, reject)=>{ if (typeof millisecond != 'number') reject(new Error('參數必須是number類型')); setTimeout(()=> { resolve(`我延遲了${millisecond}毫秒后輸出的`) }, millisecond) }) } 

我們把一個Promise封裝在一個函數里面同時返回了一個Promise,這樣比較規范。

可以看到定義的Promise有兩個參數,resolvereject

  • 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秒”

再執行setDelayresolve:“我延遲了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鏈式

不同於一般的functionbreak的方式,如果你是這樣的操作: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后面的代碼繼續執行,而同時我們又不想執行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鏈式執行,同時把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語句來捕獲錯誤。

四、后記

一直想總結一下Promiseasync/await,很多地方可能總結得不夠,已經盡力擴大篇幅了,后續有新的知識點和總結點可能會更新(未完待續),但是入門這個基本夠用了。

我們常說什么async/await的出現淘汰了Promise,可以說是大錯特錯,恰恰相反,正因為有了Promise,才有了改良版的async/await,從上面分析就可以看出,兩者是相輔相成的,缺一不可。

想學好async/await必須先精通Promise,兩者密不可分,有不同意見和改進的歡迎指導!

前端小白,大家互相交流,peace!


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM