明明有了 promise ,爲啥還需要 async await ?

爲了讓還沒聽說過這個特性的小夥伴們有一個大致瞭解,以下是一些關於該特性的簡要介紹:

async/await 是一種編寫異步代碼的新方法。在這之前編寫異步代碼使用的是回調函數和 promise。
async/await 實際是建立在 promise 之上的。因此你不能把它和回調函數搭配使用。
async/await 可以使異步代碼在形式上更接近於同步代碼。這就是它最大的價值。

語法

假設有一個 getJSON 方法,它返回一個 promise,該 promise 會被 resolve 爲一個 JSON 對象。我們想要調用該方法,輸出得到的 JSON 對象,最後返回 "done"。

以下是使用 promise 的實現方式:

const makeRequest = () =>
  getJSON()
    .then(data ={
      console.log(data)
      return "done"
    })
makeRequest()

使用 async/await 則是這樣的:

const makeRequest = async () ={
  console.log(await getJSON())
  return "done"
}

makeRequest()

使用 async/await 時有以下幾個區別:

在定義函數時我們使用了 async 關鍵字。await 關鍵字只能在使用 async 定義的函數的內部使用。所有 async 函數都會返回一個 promise,該 promise 最終 resolve 的值就是你在函數中 return 的內容。
由於第一點中的原因,你不能在頂級作用域中 await 一個函數。因爲頂級作用域不是一個 async 方法。

// this will not work in top level
// await makeRequest()
    
// this will work
makeRequest().then((result) ={
  // do something
})

await getJSON() 意味着直到 getJSON() 返回的 promise 在 resolve 之後,console.log 纔會執行並輸出 resolove 的值。

爲何使用 async/await 編寫出來的代碼更好呢?

1. 簡潔

看看我們節省了多少代碼吧。即使是在這麼一個簡單的例子中,我們也節省了可觀的代碼。我們不需要爲. then 編寫一個匿名函數來處理返回結果,也不需要創建一個 data 變量來保存我們實際用不到的值。我們還避免了代碼嵌套。這些小優點會在真實項目中變得更加明顯。

2. 錯誤處理

async/await 終於使得用同一種構造 (古老而好用的 try/catch) 處理同步和異步錯誤成爲可能。在下面這段使用 promise 的代碼中,try/catch 不能捕獲 JSON.parse 拋出的異常,因爲該操作是在 promise 中進行的。要處理 JSON.parse 拋出的異常,你需要在 promise 上調用. catch 並重復一遍異常處理的邏輯。通常在生產環境中異常處理邏輯都遠比 console.log 要複雜,因此這會導致大量的冗餘代碼。

const makeRequest = () ={
    try {
    getJSON()
      .then(result ={
        // this parse may fail
        const data = JSON.parse(result)
        console.log(data)
      })
      // uncomment this block to handle asynchronous errors
      // .catch((err) ={
      //   console.log(err)
      // })
    } catch (err) {
        console.log(err)
    }
}

現在看看使用了 async/await 的情況,catch 代碼塊現在可以捕獲 JSON.parse 拋出的異常了:

const makeRequest = async () ={
  try {
    // this parse may fail
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

3. 條件分支

假設有如下邏輯的代碼。請求數據,然後根據返回數據中的某些內容決定是直接返回這些數據還是繼續請求更多數據:

const makeRequest = () ={
  return getJSON()
    .then(data ={
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData ={
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

只是閱讀這些代碼已經夠讓你頭疼的了。一不小心你就會迷失在這些嵌套 (6 層),空格,返回語句中。(當然我們一般用請求數據的返回值作爲判斷條件不會寫成這樣,也許我這個小白會...) 在使用 async/await 改寫後,這段代碼的可讀性大大提高了:

const makeRequest = async () ={
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data    
  }
}

4. 中間值

比如你向一個 url1 發送請求,拿到返回值 1,然後用這個返回值 1 當作參數去請求 url2,拿到返回值 2,然後拿返回值 1 和返回值 2 作爲參數去請求 url3,拿到最終的返回結果。
對應的代碼看起來是這樣的:

const makeRequest = () ={
  return promise1()
    .then(value1 ={
      // do something
      return promise2(value1)
        .then(value2 ={
          // do something          
          return promise3(value1, value2)
        })
    })
}

如果 promise3 沒有用到 value1,那麼我們就可以把這幾個 promise 改成嵌套的模式。如果你不喜歡這種編碼方式,你也可以把 value1 和 value2 封裝在一個 Promsie.all 調用中以避免深層次的嵌套:

const makeRequest = () ={
  return promise1()
    .then(value1 ={
      // do something
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) ={
      // do something          
      return promise3(value1, value2)
    })
}

這種方式爲了保證可讀性而犧牲了語義。除了避免嵌套的 promise,沒有其它理由要把 value1 和 value2 放到一個數組裏。

同樣的邏輯如果換用 async/await 編寫就會非常簡單,直觀。

const makeRequest = async () ={
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}

5. 異常堆棧

假設有一段串行調用多個 promise 的代碼,在 promise 串中的某一點拋出了異常:

const makeRequest = () ={
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() ={
      throw new Error("oops");
    })
}

makeRequest()
  .catch(err ={
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })

從 promise 串返回的異常堆棧中沒有包含關於異常是從哪一個環節拋出的信息。更糟糕的是,它還會誤導你,它包含的唯一的函數名是 callAPromise,然而該函數與此異常並無關係。(這種情況下文件名和行號還是有參考價值的)。

然而,在使用了 async/await 的代碼中,異常堆棧指向了正確的函數:

const makeRequest = async () ={
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  throw new Error("oops");
}

makeRequest()
  .catch(err ={
    console.log(err);
    // output
    // Error: oops at makeRequest (index.js:7:9)
  })

這帶來的好處在本地開發環境中可能並不明顯,但當你想要在生產環境的服務器上獲取有意義的異常信息時,這會非常有用。在這種情況下,知道異常來自 makeRequest 而不是一連串的 then 調用會有意義的多。

6. 調試

最後壓軸的一點,使用 async/await 最大的優勢在於它很容易被調試。由於以下兩個原因,調試 promise 一直以來都是很痛苦的。

你不能在一個返回表達式的箭頭函數中設置斷點(因爲沒有代碼塊)

如果你在一個. then 代碼塊中使用調試器的步進 (step-over) 功能,調試器並不會進入後續的. then 代碼塊,因爲調試器只能跟蹤同步代碼的『每一步』。

通過使用 async/await,你不必再使用箭頭函數。你可以對 await 語句執行步進操作,就好像他們都是普通的同步調用一樣。

結論 async/await 是過去幾年中 JavaScript 引入的最具革命性的特性之一。它使你意識到 promise 在語法上的糟糕之處,並提供了一種簡單,直接的替代方案。

參考文章

https://loveky.github.io/2017/04/09/translte-6-reasons-why-javascripts-async-await-blows-promises-away/

轉自:Angus 安格斯

https://juejin.cn/post/6960855679208783903

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