一文搞懂promise/async await,趕超70%+的前端人

2022-09-28 14:01:19
今天給大家分享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的回撥
  • 那這樣有什麼好處呢? 看起來比早期處理的方案還要繁瑣呢?
    • 統一規範,可以增強閱讀性和擴充套件性

    • 小幅度減少回撥地獄

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 => ({name:'ice', age:22}))
       .then(res => console.log(res))
       
//{name:'ice', age:22}
登入後複製
  • 呼叫多次則會執行多次
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()
}

asyncAutomation(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進行捕獲
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. 結語

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

【相關推薦:、】

php入門到就業線上直播課: