6個Async/Await完勝Promise的原因

[譯] 6個Async/Await完勝Promise的原因

轉載自這個人   @LOVEKY · APR 9, 2017 · 2 MIN READ

原文地址:https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9

友情提醒:NodeJS自從7.6版開始已經內置了對async/await的支持。如果你還沒用過該特性,那麼接下來我會給出一系列的原因解釋爲何你應該立即開始使用它並且會結合示例代碼說明。

async/await快速入門

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

  • async/await是一種編寫異步代碼的新方法。在這之前編寫異步代碼使用的是回調函數和promise。
  • 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時有以下幾個區別:

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

    // this will not work in top level
    // await makeRequest()
        
    // this will work
    makeRequest().then((result) => {
      // do something
    })
    
  3. 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. 中間值

你可能會遇到這種情況,請求promise1,使用它的返回值請求promise2,最後使用這兩個promise的值請求promise3。對應的代碼看起來是這樣的:

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,沒有其它理由要把value1value2放到一個數組裏。

同樣的邏輯如果換用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一直以來都是很痛苦的。

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

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

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

結論

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

疑慮

一些你在使用此特性可能出現的疑慮:

  • 它使得異步代碼不那麼明顯了:我們的眼睛已經學會了通過尋找回調函數或.then來發現異步代碼,因此需要一段時間來適應新的標識符。C#中已經內置此特性多年了,熟悉的人都知道這只是一個小小的,暫時的不便。
  • Node 7不是一個LTS發佈版:是的,但是Node 8將在下個月發佈。同時,遷移你的代碼到最新版本可能根本不需要任何代價。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章