7 張圖,20 分鐘搞定 async-await 原理!別拖了!

前言


大家好,我是林三心,以最通俗的話,講最難的知識點是我寫文章的宗旨

之前我發過一篇手寫 Promise 原理,最通俗易懂的版本!!!,帶大家基本瞭解了Promise內部的實現原理,而提到Promise,就不得不提一個東西,那就是async/awaitasync/await是一個很重要的語法糖,他的作用是用同步的方式,執行異步操作。那麼今天我就帶大家一起實現一下async/await吧!!!

async/await 用法

其實你要實現一個東西之前,最好是先搞清楚這兩樣東西

有什麼用?

async/await的用處就是:用同步方式,執行異步操作,怎麼說呢?舉個例子

比如我現在有一個需求:先請求完接口1,再去請求接口2,我們通常會這麼做

function request(num) { // 模擬接口請求
  return new Promise(resolve ={
    setTimeout(() ={
      resolve(num * 2)
    }, 1000)
  })
}

request(1).then(res1 ={
  console.log(res1) // 1秒後 輸出 2

  request(2).then(res2 ={
    console.log(res2) // 2秒後 輸出 4
  })
})

或者我現在又有一個需求:先請求完接口1,再拿接口1返回的數據,去當做接口2的請求參數,那我們也可以這麼做

request(5).then(res1 ={
  console.log(res1) // 1秒後 輸出 10

  request(res1).then(res2 ={
    console.log(res2) // 2秒後 輸出 20
  })
})

其實這麼做是沒問題的,但是如果嵌套的多了,不免有點不雅觀,這個時候就可以用async/await來解決了

async function fn () {
  const res1 = await request(5)
  const res2 = await request(res1)
  console.log(res2) // 2秒後輸出 20
}
fn()

是怎麼用?

還是用剛剛的例子

需求一:

async function fn () {
  await request(1)
  await request(2)
  // 2秒後執行完
}
fn()

需求二:

async function fn () {
  const res1 = await request(5)
  const res2 = await request(res1)
  console.log(res2) // 2秒後輸出 20
}
fn()

截屏 2021-09-11 下午 9.57.58.png

其實就類似於生活中的排隊,咱們生活中排隊買東西,肯定是要上一個人買完,才輪到下一個人。而上面也一樣,在async函數中,await規定了異步操作只能一個一個排隊執行,從而達到用同步方式,執行異步操作的效果,這裏注意了:await 只能在 async 函數中使用,不然會報錯哦

剛剛上面的例子await後面都是跟着異步操作Promise,那如果不接Promise會怎麼樣呢?

function request(num) { // 去掉Promise
  setTimeout(() ={
    console.log(num * 2)
  }, 1000)
}

async function fn() {
  await request(1) // 2
  await request(2) // 4
  // 1秒後執行完  同時輸出
}
fn()

可以看出,如果await後面接的不是Promise的話,有可能其實是達不到排隊的效果的

說完await,咱們聊聊async吧,async是一個位於 function 之前的前綴,只有async函數中,才能使用await。那async執行完是返回一個什麼東西呢?

async function fn () {}
console.log(fn) // [AsyncFunction: fn]
console.log(fn()) // Promise {<fulfilled>: undefined}

可以看出,async函數執行完會自動返回一個狀態爲fulfilled的 Promise,也就是成功狀態,但是值卻是 undefined,那要怎麼才能使值不是 undefined 呢?很簡單,函數有return返回值就行了

async function fn (num) {
  return num
}
console.log(fn) // [AsyncFunction: fn]
console.log(fn(10)) // Promise {<fulfilled>: 10}
fn(10).then(res => console.log(res)) // 10

可以看出,此時就有值了,並且還能使用then方法進行輸出

總結

總結一下async/await的知識點

什麼是語法糖?

前面說了,async/await是一種語法糖,誒!好多同學就會問,啥是語法糖呢?我個人理解就是,語法糖就是一個東西,這個東西你就算不用他,你用其他手段也能達到這個東西同樣的效果,但是可能就沒有這個東西這麼方便了。

迴歸正題,async/await是一種語法糖,那就說明用其他方式其實也可以實現他的效果,我們今天就是講一講怎麼去實現async/await,用到的是 ES6 裏的迭代函數——generator函數

generator 函數

基本用法

generator函數跟普通函數在寫法上的區別就是,多了一個星號*,並且只有在generator函數中才能使用yield,什麼是yield呢,他相當於generator函數執行的中途暫停點,比如下方有 3 個暫停點。而怎麼才能暫停後繼續走呢?那就得使用到next方法next方法執行後會返回一個對象,對象中有value 和 done兩個屬性

function* gen() {
  yield 1
  yield 2
  yield 3
}
const g = gen()
console.log(g.next()) // { value: 1, donefalse }
console.log(g.next()) // { value: 2, donefalse }
console.log(g.next()) // { value: 3, donefalse }
console.log(g.next()) // { value: undefined, donetrue }

可以看到最後一個是 undefined,這取決於你 generator 函數有無返回值

function* gen() {
  yield 1
  yield 2
  yield 3
  return 4
}
const g = gen()
console.log(g.next()) // { value: 1, donefalse }
console.log(g.next()) // { value: 2, donefalse }
console.log(g.next()) // { value: 3, donefalse }
console.log(g.next()) // { value: 4, donetrue }

截屏 2021-09-11 下午 9.46.17.png

yield 後面接函數

yield 後面接函數的話,到了對應暫停點 yield,會馬上執行此函數,並且該函數的執行返回值,會被當做此暫停點對象的value

function fn(num) {
  console.log(num)
  return num
}
function* gen() {
  yield fn(1)
  yield fn(2)
  return 3
}
const g = gen()
console.log(g.next()) 
// 1
// { value: 1, donefalse }
console.log(g.next())
// 2
//  { value: 2, donefalse }
console.log(g.next()) 
// { value: 3, donetrue }

yield 後面接 Promise

前面說了,函數執行返回值會當做暫停點對象的 value 值,那麼下面例子就可以理解了,前兩個的 value 都是 pending 狀態的 Promise 對象

function fn(num) {
  return new Promise(resolve ={
    setTimeout(() ={
      resolve(num)
    }, 1000)
  })
}
function* gen() {
  yield fn(1)
  yield fn(2)
  return 3
}
const g = gen()
console.log(g.next()) // { value: Promise { <pending> }donefalse }
console.log(g.next()) // { value: Promise { <pending> }donefalse }
console.log(g.next()) // { value: 3, donetrue }

截屏 2021-09-11 下午 10.51.38.png

其實我們想要的結果是,兩個 Promise 的結果1 和 2,那怎麼做呢?很簡單,使用 Promise 的 then 方法就行了

const g = gen()
const next1 = g.next()
next1.value.then(res1 ={
  console.log(next1) // 1秒後輸出 { value: Promise { 1 }donefalse }
  console.log(res1) // 1秒後輸出 1

  const next2 = g.next()
  next2.value.then(res2 ={
    console.log(next2) // 2秒後輸出 { value: Promise { 2 }donefalse }
    console.log(res2) // 2秒後輸出 2
    console.log(g.next()) // 2秒後輸出 { value: 3, donetrue }
  })
})

截屏 2021-09-11 下午 10.38.37.png

next 函數傳參

generator 函數可以用next方法來傳參,並且可以通過yield來接收這個參數,注意兩點

function* gen() {
  const num1 = yield 1
  console.log(num1)
  const num2 = yield 2
  console.log(num2)
  return 3
}
const g = gen()
console.log(g.next()) // { value: 1, donefalse }
console.log(g.next(11111))
// 11111
//  { value: 2, donefalse }
console.log(g.next(22222)) 
// 22222
// { value: 3, donetrue }

截屏 2021-09-11 下午 10.53.02.png

Promise+next 傳參

前面講了

那這兩個組合起來會是什麼樣呢?

function fn(nums) {
  return new Promise(resolve ={
    setTimeout(() ={
      resolve(nums * 2)
    }, 1000)
  })
}
function* gen() {
  const num1 = yield fn(1)
  const num2 = yield fn(num1)
  const num3 = yield fn(num2)
  return num3
}
const g = gen()
const next1 = g.next()
next1.value.then(res1 ={
  console.log(next1) // 1秒後同時輸出 { value: Promise { 2 }donefalse }
  console.log(res1) // 1秒後同時輸出 2

  const next2 = g.next(res1) // 傳入上次的res1
  next2.value.then(res2 ={
    console.log(next2) // 2秒後同時輸出 { value: Promise { 4 }donefalse }
    console.log(res2) // 2秒後同時輸出 4

    const next3 = g.next(res2) // 傳入上次的res2
    next3.value.then(res3 ={
      console.log(next3) // 3秒後同時輸出 { value: Promise { 8 }donefalse }
      console.log(res3) // 3秒後同時輸出 8

       // 傳入上次的res3
      console.log(g.next(res3)) // 3秒後同時輸出 { value: 8, donetrue }
    })
  })
})

截屏 2021-09-11 下午 11.05.44.png

實現 async/await

其實上方的generator函數Promise+next傳參,就很像async/await了,區別在於

截屏 2021-09-11 下午 11.53.41.png

那我們怎麼辦呢?我們可以封裝一個高階函數。什麼是高階函數呢?高階函數的特點是:參數是函數,返回值也可以是函數。下方的highorderFn就是一個高階函數

function highorderFn(函數) {
    // 一系列處理
    
    return 函數
}

我們可以封裝一個高階函數,接收一個 generator 函數,並經過一系列處理,返回一個具有 async 函數功能的函數

function generatorToAsync(generatorFn) {
  // 經過一系列處理
  
  return 具有async函數功能的函數
}

返回值 Promise

之前我們說到,async 函數的執行返回值是一個 Promise,那我們要怎麼實現相同的結果呢

function* gen() {

}

const asyncFn = generatorToAsync(gen)

console.log(asyncFn()) // 期望這裏輸出 Promise

其實很簡單,generatorToAsync函數裏做一下處理就行了

function* gen() {

}
function generatorToAsync (generatorFn) {
  return function () {
    return new Promise((resolve, reject) ={

    })
  }
}

const asyncFn = generatorToAsync(gen)

console.log(asyncFn()) // Promise

加入一系列操作

咱們把之前的處理代碼,加入generatorToAsync函數

function fn(nums) {
  return new Promise(resolve ={
    setTimeout(() ={
      resolve(nums * 2)
    }, 1000)
  })
}
function* gen() {
  const num1 = yield fn(1)
  const num2 = yield fn(num1)
  const num3 = yield fn(num2)
  return num3
}
function generatorToAsync(generatorFn) {
  return function () {
    return new Promise((resolve, reject) ={
      const g = generatorFn()
      const next1 = g.next()
      next1.value.then(res1 ={

        const next2 = g.next(res1) // 傳入上次的res1
        next2.value.then(res2 ={

          const next3 = g.next(res2) // 傳入上次的res2
          next3.value.then(res3 ={

            // 傳入上次的res3
            resolve(g.next(res3).value)
          })
        })
      })
    })
  }
}

const asyncFn = generatorToAsync(gen)

asyncFn().then(res => console.log(res)) // 3秒後輸出 8

可以發現,咱們其實已經實現了以下的async/await的結果了

async function asyncFn() {
  const num1 = await fn(1)
  const num2 = await fn(num1)
  const num3 = await fn(num2)
  return num3
}
asyncFn().then(res => console.log(res)) // 3秒後輸出 8

完善代碼

上面的代碼其實都是死代碼,因爲一個 async 函數中可能有 2 個 await,3 個 await,5 個 await ,其實 await 的個數是不確定的。同樣類比,generator 函數中,也可能有 2 個 yield,3 個 yield,5 個 yield,所以咱們得把代碼寫成活的纔行

function generatorToAsync(generatorFn) {
  return function() {
    const gen = generatorFn.apply(this, arguments) // gen有可能傳參

    // 返回一個Promise
    return new Promise((resolve, reject) ={

      function go(key, arg) {
        let res
        try {
          res = gen[key](arg) // 這裏有可能會執行返回reject狀態的Promise
        } catch (error) {
          return reject(error) // 報錯的話會走catch,直接reject
        }

        // 解構獲得value和done
        const { value, done } = res
        if (done) {
          // 如果done爲true,說明走完了,進行resolve(value)
          return resolve(value)
        } else {
          // 如果done爲false,說明沒走完,還得繼續走

          // value有可能是:常量,Promise,Promise有可能是成功或者失敗
          return Promise.resolve(value).then(val => go('next', val)err => go('throw', err))
        }
      }

      go("next") // 第一次執行
    })
  }
}

const asyncFn = generatorToAsync(gen)

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

這樣的話,無論是多少個 yield 都會排隊執行了,咱們把代碼寫成活的了

示例

async/await版本

async function asyncFn() {
  const num1 = await fn(1)
  console.log(num1) // 2
  const num2 = await fn(num1)
  console.log(num2) // 4
  const num3 = await fn(num2)
  console.log(num3) // 8
  return num3
}
const asyncRes = asyncFn()
console.log(asyncRes) // Promise
asyncRes.then(res => console.log(res)) // 8

使用generatorToAsync函數的版本

function* gen() {
  const num1 = yield fn(1)
  console.log(num1) // 2
  const num2 = yield fn(num1)
  console.log(num2) // 4
  const num3 = yield fn(num2)
  console.log(num3) // 8
  return num3
}

const genToAsync = generatorToAsync(gen)
const asyncRes = genToAsync()
console.log(asyncRes) // Promise
asyncRes.then(res => console.log(res)) // 8

結語

喜歡的朋友可以支持一下我,點擊一下點贊或者在看,也可以關注我的公衆號【前端之神】,加我好友,拉你進【千人學習大羣】哦!

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/RFK0QWeE-jXRUJUR0KDpSA