前端人70%以上 不瞭解的promise/async await

語言: CN / TW / HK

theme: nico

前言

  今天給大家分享promise,筆者將從早期的非同步程式碼的困境、promise出現解決了什麼問題、非同步回撥地獄的終極方案並且實現async await的核心語法,其實async/await只是generator+promise的一個變種而已。這是堅持寫部落格的第三週,堅持下去事情總是會變好!

1. 早期非同步程式碼困境

  • 眾所周知,js是單執行緒的,耗時操作都是交給瀏覽器來處理,等時間到了從佇列中取出執行,設計到事件迴圈的概念,筆者也分享過,可以看以下,理解了可以更好的理解promise
  • 我以一個需求為切入點,我模擬網路請求(非同步操作)
    • 如果網路請求成功了,你告知我成功了
    • 如果網路請求失敗了,你告知我失敗了

1.1 大聰明做法

``` function requestData(url) { setTimeout(() => { if (url === 'iceweb.io') { return '請求成功' } return '請求失敗' }, 3000) }

const result = requestData('iceweb.io')

console.log(result) //undefined `` - 首先你要理解js程式碼的執行順序,而不是是想當然的,程式碼其實並不是按照你書寫的順序執行的。 - 那麼為什麼是undefined呢? - 首先當我執行requestData函式,開始執行函式。遇到了非同步操作不會阻塞後面程式碼執行的,因為js是單執行緒的,所以你寫的return成功或者失敗並沒有返回,那我這個函式中,拋開非同步操作,裡面並沒有返回值,所以值為undefined`。

2.2 早期正確做法

``` function requestData(url, successCB, failureCB) { setTimeout(() => { if (url === 'iceweb.io') { successCB('我成功了,把獲取到的資料傳出去', [{name:'ice', age:22}]) } else { failureCB('url錯誤,請求失敗') } }, 3000) }

//3s後 回撥successCB //我成功了,把獲取到的資料傳出去 [ { name: 'ice', age: 22 } ] requestData('iceweb.io', (res, data) => console.log(res, data), rej => console.log(rej))

//3s後回撥failureCB //url錯誤,請求失敗 requestData('icexxx.io', res => console.log(res) ,rej => console.log(rej)) ```

  • 早期解決方案都是傳入兩個回撥,一個失敗的,一個成功的。那很多開發者會問這不是挺好的嗎?挺簡單的,js中函式是一等公民,可以傳來傳去,但是這樣太靈活了,沒有規範。
  • 如果使用的是框架,還要閱讀一下框架原始碼,正確失敗的傳實參的順序,如果傳參順序錯誤這樣是非常危險的。

2. Promise

  • Promise(承諾),給予呼叫者一個承諾,過一會返回資料給你,就可以建立一個promise物件
  • 當我們new一個promise,此時我們需要傳遞一個回撥函式,這個函式為立即執行的,稱之為(executor)
  • 這個回撥函式,我們需要傳入兩個引數回撥函式,reslove,reject(函式可以進行傳參)
    • 當執行了reslove函式,會回撥promise物件的.then函式
    • 當執行了reject函式,會回撥promise物件的.catche函式

2.1 Executor立即執行

new Promise((resolve, reject) => { console.log(`executor 立即執行`) }) - 傳入的executor是立即執行的

2.2 requestData 重構

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url === 'iceweb.io') { //只能傳遞一個引數 resolve('我成功了,把獲取到的資料傳出去') } else { reject('url錯誤,請求失敗') } }, 3000)
}) }

//1. 請求成功 requestData('iceweb.io').then(res => { //我成功了,把獲取到的資料傳出去 console.log(res) })

//2. 請求失敗

//2.2 第一種寫法 //url錯誤,請求失敗 requestData('iceweb.org').then(res => {},rej => console.log(rej))

//2.2 第二種寫法 //url錯誤,請求失敗 requestData('iceweb.org').catch(e => console.log(e)) ```

  • 在函式中,new這個類的時候,傳入的回撥函式稱之為executor(會被Promise類中自動執行)
  • 在正確的時候呼叫resolve函式,失敗的時候呼叫reject函式,把需要的引數傳遞出去。
  • 異常處理
    • 其中在.then方法中可以傳入兩個回撥,您也可以檢視Promise/A+規範
      • 第一個則是fulfilled的回撥
      • 第二個則是rejected的回撥
  • 那這樣有什麼好處呢? 看起來比早期處理的方案還要繁瑣呢?
    1. 統一規範,可以增強閱讀性和擴充套件性
    2. 小幅度減少回撥地獄

2.3 promise的狀態

  • 首先先給大家舉個栗子,把程式碼抽象為現實的栗子
    • 你答應你女朋友,下週末帶她去吃好吃的 (還未到下週末,此時狀態為待定狀態)
    • 時間飛快,今天就是週末了,你和你女友一起吃了烤肉、甜點、奶茶...(已兌現狀態
    • 時間飛快,今天就是週末了,正打算出門。不巧產品經理,因為線上出現的緊急問題,需要回公司解決一下,你(為了生活)只能委婉的拒絕一下女友,並且說明一下緣由(已拒絕狀態)
  • 使用promise的時候,給它一個承諾,我們可以將他劃分為三個階段
    • pending(待定),執行了executor,狀態還在等待中,沒有被兌現,也沒有被拒絕
    • fulfilled(已兌現),執行了resolve函式則代表了已兌現狀態
    • rejected(已拒絕),執行了reject函式則代表了已拒絕狀態
  • 首先,狀態只要從待定狀態,變為其他狀態,則狀態不能再改變

思考以下程式碼: ``` const promise = new Promise((resolve, reject) => { setTimeout(() => { reject('失敗') resolve('成功') }, 3000); })

promise.then(res => console.log(res)).catch(err => console.log(err))

//失敗 ```

  • 當我呼叫reject之後,在呼叫resolve是無效的,因為狀態已經發生改變,並且是不可逆的。

2.4 resolve不同值的區別

  • 如果resolve傳入一個普通的值或者物件,只能傳遞接受一個引數,那麼這個值會作為then回撥的引數 ``` const promise = new Promise((resolve, reject) => { resolve({name: 'ice', age: 22}) })

promise.then(res => console.log(res))

// {name: 'ice', age: 22} ```

  • 如果resolve中傳入的是另外一個Promise,那麼這個新Promise會決定原Promise的狀態 ``` const promise = new Promise((resolve, reject) => { resolve(new Promise((resolve, reject) => { setTimeout(() => { resolve('ice') }, 3000); })) })

promise.then(res => console.log(res))

//3s後 ice ```

  • 如果resolve中傳入的是一個物件,並且這個物件有實現then方法,那麼會執行該then方法,then方法會傳入resolvereject函式。此時的promise狀態取決於你呼叫了resolve,還是reject函式。這種模式也稱之為: thenable

``` const promise = new Promise((resolve, reject) => { resolve({ then(res, rej) { res('hi ice') } }) })

promise.then(res => console.log(res))

// hi ice ```

2.5 Promise的例項方法

  • 例項方法,存放在Promise.prototype上的方法,也就是Promise的顯示原型上,當我new Promise的時候,會把返回的改物件的 promise[[prototype]](隱式原型) === Promise.prototype (顯示原型)
  • 即new返回的物件的隱式原型指向了Promise的顯示原型

2.5.1 then方法

2.5.1.1 then的引數
  • then方法可以接受引數,一個引數為成功的回撥,另一個引數為失敗的回撥,前面重構requestData中有演練過。

``` const promise = new Promise((resolve, reject) => { resolve('request success') // reject('request error') })

promise.then(res => console.log(res), rej => console.log(rej))

//request success - 如果只捕獲錯誤,還可以這樣寫 - 因為第二個引數是捕獲異常的,第一個可以寫個`null`或`""`佔位 const promise = new Promise((resolve, reject) => { // resolve('request success') reject('request error') })

promise.then(null, rej => console.log(rej))

//request error ```

2.5.1.2 then的多次呼叫

``` const promise = new Promise((resolve, reject) => { resolve('hi ice') })

promise.then(res => console.log(res)) promise.then(res => console.log(res)) promise.then(res => console.log(res)) ``` - 呼叫多次則會執行多次

2.5.1.3 then的返回值
  • then方法是有返回值的,它的返回值是promise,但是是promise那它的狀態如何決定呢?接下來讓我們一探究竟。
2.5.1.3.1 返回一個普通值 狀態:fulfilled

``` const promise = new Promise((resolve, reject) => { resolve('hi ice') })

promise.then(res => ({name:'ice', age:22})) .then(res => console.log(res))

//{name:'ice', age:22} ```

  • 返回一個普通值,則相當於主動呼叫Promise.resolve,並且把返回值作為實參傳遞到then方法中。
  • 如果沒有返回值,則相當於返回undefined
2.5.1.3.2 明確返回一個promise 狀態:fulfilled

``` const promise = new Promise((resolve, reject) => { resolve('hi ice') })

promise.then(res => { return new Promise((resolve, reject) => { resolve('then 的返回值') }) }).then(res => console.log(res))

//then 的返回值 `` - 主動返回一個promise物件,狀態和你呼叫resolve,還是reject`有關

2.5.1.3.3 返回一個thenable物件 狀態:fulfilled

``` const promise = new Promise((resolve, reject) => { resolve('hi ice') })

promise.then(res => { return { then(resolve, reject) { resolve('hi webice') } } }).then(res => console.log(res))

//hi webice `` - 返回了一個thenable物件,其狀態取決於你是呼叫了resolve,還是reject`

2.5.2 catch方法

2.5.2.1 catch的多次呼叫

``` const promise = new Promise((resolve, reject) => { reject('ice error') })

promise.catch(err => console.log(err)) promise.catch(err => console.log(err)) promise.catch(err => console.log(err))

//ice error //ice error //ice error ```

2.5.2.2 catch的返回值
  • catch方法是有返回值的,它的返回值是promise,但是是promise那它的狀態如何決定呢?接下來讓我們一探究竟。
  • 如果返回值明確一個promise或者thenble物件,取決於你呼叫了resolve還是reject
2.5.2.2.1 返回一個普通物件

``` const promise = new Promise((resolve, reject) => { reject('ice error') })

promise.catch(err => ({name:'ice', age: 22})).then(res => console.log(res))

//{name:'ice', age: 22} ```

2.5.2.2.2 明確返回一個promise

``` const promise = new Promise((resolve, reject) => { reject('ice error') })

promise.catch(err => { return new Promise((resolve, reject) => { reject('ice error promise') }) }).catch(res => console.log(res))

//ice error promise ```

  • 此時new Promise()呼叫了reject函式,則會被catch捕獲到
2.5.2.2.3 返回thenble物件

``` const promise = new Promise((resolve, reject) => { reject('ice error') })

promise.catch(err => { return { then(resolve, reject) { reject('ice error then') } } }).catch(res => console.log(res))

//ice error then ```

2.5.3 finally方法

  • ES9(2018)新例項方法
  • finally(最後),無論promise狀態是fulfilled還是rejected都會執行一次finally方法 ``` const promise = new Promise((resolve, reject) => { resolve('hi ice') })

promise.then(res => console.log(res)).finally(() => console.log('finally execute'))

//finally execute ```

2.6 Promise中的類方法/靜態方法

2.6.1 Promise.reslove

Promise.resolve('ice') //等價於 new Promise((resolve, reject) => resolve('ice'))

  • 有的時候,你已經預知了狀態的結果為fulfilled,則可以用這種簡寫方式

2.6.2 Promise.reject

Promise.reject('ice error') //等價於 new Promise((resolve, reject) => reject('ice error'))

  • 有的時候,你已經預知了狀態的結果為rejected,則可以用這種簡寫方式

2.6.3 Promise.all

fulfilled 狀態 ``` const promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi ice') }, 1000); })

const promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi panda') }, 2000); })

const promise3 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi grizzly') }, 3000); })

Promise.all([promise1, promise2, promise3]).then(res => console.log(res))

//[ 'hi ice', 'hi panda', 'hi grizzly' ] `` - all方法的引數傳入為一個可迭代物件,返回一個promise,只有三個都為resolve狀態的時候才會呼叫.then方法。 - 只要有一個promise的狀態為rejected,則會回撥.catch`方法

rejected狀態

``` const promise1 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi ice') }, 1000); })

const promise2 = new Promise((resolve, reject) => { setTimeout(() => { reject('hi panda') }, 2000); })

const promise3 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi grizzly') }, 3000); })

Promise.all([promise1, promise2, promise3]).then(res => console.log(res)).catch(err => console.log(err))

//hi panda ``` - 當遇到rejectd的時候,後續的promise結果我們是獲取不到,並且會把reject的實參,傳遞給catch的err形參中

2.6.4 Promise.allSettled

  • 上面的Promise.all有一個缺陷,就是當遇到一個rejected的狀態,那麼對於後面是resolve或者reject的結果我們是拿不到的
  • ES11 新增語法Promise.allSettled,無論狀態是fulfilled/rejected都會把引數返回給我們

所有promise都有結果 ``` const promise1 = new Promise((resolve, reject) => { setTimeout(() => { reject('hi ice') }, 1000); })

const promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi panda') }, 2000); })

const promise3 = new Promise((resolve, reject) => { setTimeout(() => { reject('hi grizzly') }, 3000); })

Promise.allSettled([promise1, promise2, promise3]).then(res => console.log(res))

/ [ { status: 'rejected', reason: 'hi ice' }, { status: 'fulfilled', value: 'hi panda' }, { status: 'rejected', reason: 'hi grizzly' } ] / ``` - 該方法會在所有的Promise都有結果,無論是fulfilled,還是rejected,才會有最終的結果

其中一個promise沒有結果

``` const promise1 = new Promise((resolve, reject) => { setTimeout(() => { reject('hi ice') }, 1000); })

const promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi panda') }, 2000); })

const promise3 = new Promise((resolve, reject) => {})

Promise.allSettled([promise1, promise2, promise3]).then(res => console.log(res)) // 什麼都不列印 ```

  • 其中一個promise沒有結果,則什麼都結果都拿不到

2.6.5 Promise.race

  • race(競爭競賽)
  • 優先獲取第一個返回的結果,無論結果是fulfilled還是rejectd ``` const promise1 = new Promise((resolve, reject) => { setTimeout(() => { reject('hi error') }, 1000); })

const promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi panda') }, 2000); })

Promise.race([promise1, promise2]) .then(res => console.log(res)) .catch(e => console.log(e))

//hi error ```

2.6.6 Promise.any

  • 與race類似,只獲取第一個狀態為fulfilled,如果全部為rejected則報錯AggregateError

``` const promise1 = new Promise((resolve, reject) => { setTimeout(() => { reject('hi error') }, 1000); })

const promise2 = new Promise((resolve, reject) => { setTimeout(() => { resolve('hi panda') }, 2000); })

Promise.any([promise1, promise2]) .then(res => console.log(res)) .catch(e => console.log(e))

//hi panda ```

3. Promise的回撥地獄 (進階)

  • 我還是以一個需求作為切入點,把知識點嚼碎了,一點一點喂進你們嘴裡。
    • 當我傳送網路請求的時候,需要拿到這次網路請求的資料,再發送網路請求,就這樣重複三次,才能拿到我最終的結果。

3.1 臥龍解法

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes('iceweb')) { resolve(url) } else { reject('請求錯誤') } }, 1000); }) }

requestData('iceweb.io').then(res => { requestData(iceweb.org ${res}).then(res => { requestData(iceweb.com ${res}).then(res => { console.log(res) }) }) })

//iceweb.com iceweb.org iceweb.io ```

  • 雖然能夠實現,但是多層程式碼的巢狀,可讀性非常差,我們把這種多層次程式碼巢狀稱之為回撥地獄

3.2 鳳雛解法

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes('iceweb')) { resolve(url) } else { reject('請求錯誤') } }, 1000); }) }

requestData('iceweb.io').then(res => { return requestData(iceweb.org ${res}) }).then(res => { return requestData(iceweb.com ${res}) }).then(res => { console.log(res) })

//iceweb.com iceweb.org iceweb.io ```

  • 利用了then鏈式呼叫這一特性,返回了一個新的promise,但是不夠優雅,思考一下能不能寫成同步的方式呢?

3.3 生成器+Promise解法

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes('iceweb')) { resolve(url) } else { reject('請求錯誤') } }, 1000); }) }

function* getData(url) { const res1 = yield requestData(url) const res2 = yield requestData(res1) const res3 = yield requestData(res2)

console.log(res3) }

const generator = getData('iceweb.io')

generator.next().value.then(res1 => { generator.next(iceweb.org ${res1}).value.then(res2 => { generator.next(iceweb.com ${res2}).value.then(res3 => { generator.next(res3) }) }) })

//iceweb.com iceweb.org iceweb.io ```

  • 大家可以發現我們的getData已經變為同步的形式,可以拿到我最終的結果了。那麼很多同學會問,generator一直呼叫.next不是也產生了回撥地獄嗎?
  • 其實不用關心這個,我們可以發現它這個是有規律的,我們可以封裝成一個自動化執行的函式,我們就不用關心內部是如何呼叫的了。

3.4 自動化執行函式封裝

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes('iceweb')) { resolve(url) } else { reject('請求錯誤') } }, 1000); }) }

function* getData() { const res1 = yield requestData('iceweb.io') const res2 = yield requestData(iceweb.org ${res1}) const res3 = yield requestData(iceweb.com ${res2})

console.log(res3) }

//自動化執行 async await相當於自動幫我們執行.next function asyncAutomation(genFn) { const generator = genFn()

const _automation = (result) => { let nextData = generator.next(result) if(nextData.done) return

nextData.value.then(res => {
  _automation(res)
})

}

_automation() }

syncAutomation(getData)

//iceweb.com iceweb.org iceweb.io `` - 利用promise+生成器的方式變相實現解決回撥地獄問題,其實就是async await的一個變種而已 - 最早為**TJ**實現,**前端大神人物** - async await核心程式碼就類似這些,內部主動幫我們呼叫.next`方法

3.5 最終解決回撥地獄的辦法

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes('iceweb')) { resolve(url) } else { reject('請求錯誤') } }, 1000); }) }

async function getData() { const res1 = await requestData('iceweb.io') const res2 = await requestData(iceweb.org ${res1}) const res3 = await requestData(iceweb.com ${res2})

console.log(res3) }

getData()

//iceweb.com iceweb.org iceweb.io `` - 你會驚奇的發現,只要把getData生成器函式函式,改為async函式,yeild的關鍵字替換為await`就可以實現非同步程式碼同步寫法了。

4. async/await 剖析

  • async(非同步的)
  • async 用於申明一個非同步函式

4.1 async內部程式碼同步執行

  • 非同步函式的內部程式碼執行過程和普通的函式是一致的,預設情況下也是會被同步執行 ``` async function sayHi() { console.log('hi ice') }

sayHi()

//hi ice ```

4.2 非同步函式的返回值

  • 非同步函式的返回值和普通返回值有所區別

    • 普通函式主動返回什麼就返回什麼,不返回為undefined
    • 非同步函式的返回值特點
      • 明確有返回一個普通值,相當於Promise.resolve(返回值)
      • 返回一個thenble物件則由,then方法中的resolve,或者reject有關
      • 明確返回一個promise,則由這個promise決定
  • 非同步函式中可以使用await關鍵字,現在在全域性也可以進行await,但是不推薦。會阻塞主程序的程式碼執行

4.3 非同步函式的異常處理

  • 如果函式內部中途發生錯誤,可以通過try catch的方式捕獲異常
  • 如果函式內部中途發生錯誤,也可以通過函式的返回值.catch進行捕獲

```js

async function sayHi() { console.log(res) } sayHi().catch(e => console.log(e))

//或者

async function sayHi() { try { console.log(res) }catch(e) { console.log(e) } }

sayHi()

//ReferenceError: res is not defined ```

4.4 await 關鍵字

  • 非同步函式中可以使用await關鍵字,普通函式不行
  • await特點
    • 通常await關鍵字後面都是跟一個Promise
      • 可以是普通值
      • 可以是thenble
      • 可以是Promise主動呼叫resolve或者reject
    • 這個promise狀態變為fulfilled才會執行await後續的程式碼,所以await後面的程式碼,相當於包括在.then方法的回撥中,如果狀態變為rejected,你則需要在函式內部try catch,或者進行鏈式呼叫進行.catch操作

``` function requestData(url) { return new Promise((resolve, reject) => { setTimeout(() => { if (url.includes('iceweb')) { resolve(url) } else { reject('請求錯誤') } }, 1000); }) }

async function getData() { const res = await requestData('iceweb.io') console.log(res) }

getData()

// iceweb.io ```

5. 結語

  • 如果現在真的看不到未來是怎樣,你就不如一直往前走,不知道什麼時候天亮,去奔跑就好,跑著跑著天就亮了。