使用 Async / Await 來編寫簡明的異步代碼

原文連接:https://blog.patricktriest.com/what-is-async-await-why-should-you-care/
複製代碼

中止書寫回調函數並愛上ES8

之前,JavaScript項目會逐漸‘失去控制’,其中主要一個緣由就是採用傳統的回調函數處理異步任務時,一旦業務邏輯比較複雜,咱們就不免書寫一些冗長、複雜、嵌套的代碼塊(回調地獄),這會嚴重下降代碼的可讀性與可維護性。如今,JavaScript提供了一種新的語法糖來取代回調函數,使咱們可以編寫簡明、可讀性高的異步代碼。javascript

背景

AJAX

先來回顧一下歷史。在20世紀90年代後期,Ajax是異步JavaScript的第一個重大突破。這一技術容許網站在加載HTML後獲取並顯示最新的數據,這是一個革命性的想法。在這以前,大多數網站會再次下載整個頁面來顯示更新的內容。這一技術(在jQuery中以ajax的名稱流行)主導了2000-2010的web開發而且Ajax是目前網站用來獲取數據的主要技術,可是XML在很大程度上取代了JSON。java

NodeJS

當NodeJS在2009年首次發佈時,服務器端環境的主要焦點是容許程序優雅地處理併發性。大多數服務器端語言經過阻塞代碼來處理I/O操做,直到操做完成爲止。相反,NodeJS使用的是事件循環機制,這樣開發人員能夠在非阻塞異步操做完成後,調用回調函數來處理邏輯(相似於Ajax的工做方式)。web

Promises

幾年後,NodeJS和瀏覽器環境中出現了一種新的標準,稱爲"Promise",Promise提供了一種強大的、標準化的方式來組成異步操做。Promise仍然使用基於回調的格式,但爲鏈式和組合異步操做提供了一致的語法。在2015年,由流行的開源庫所倡導的Promise最終被添加爲JavaScript的原生特性。 Promise是一個不錯的改進,但它們仍然經常是一些冗長而難以閱讀的代碼塊的緣由。 而如今有了一個解決方案。 Async/Await是一種新的語法(從.net和C#中借用),它容許咱們編寫Promise,但它們看起來像是同步代碼,沒有回調,能夠用來簡化幾乎任何現有的JS應用程序。Async/Await是JavaScript語言的新增的特性,在ES7中被正式添加爲JavaScript的原生特性。ajax

示例

咱們將經過一些代碼示例來展現Async/Await的魅力編程

:運行下面的示例不須要任何庫。Async/Await已經被最新版本的Chrome、FireFox、Safari、Edge徹底支持,你能夠在你的瀏覽器控制檯裏運行例子。Async/Await須要運行在NodeJS 7.6版本及以上,同時也被Babel、TypeScript轉譯器支持。因此Async/Await能夠被用於實際開發之中。api

準備

咱們會使用一個虛擬的API類,你也能夠在你的電腦上運行。這個類經過返回promise來模擬異步請求。正常狀況下,promise被調用後,200ms後會對數據進行處理。數組

class Api {
  constructor () {
    this.user = { id: 1, name: 'test' }
    this.friends = [ this.user, this.user, this.user ]
    this.photo = 'not a real photo'
  }

  getUser () {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.user), 200)
    })
  }

  getFriends (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.friends.slice()), 200)
    })
  }

  getPhoto (userId) {
    return new Promise((resolve, reject) => {
      setTimeout(() => resolve(this.photo), 200)
    })
  }

  throwError () {
    return new Promise((resolve, reject) => {
      setTimeout(() => reject(new Error('Intentional Error')), 200)
    })
  }
}
複製代碼

每一個示例依次執行以下三個操做: 獲取一個用戶的信息,獲取該用戶的朋友, 獲取該用戶的照片。在最後,咱們會在控制檯中打印這些結果。promise

方法一 --- Nested Promise Callback Functions

使用嵌套的promise回調函數瀏覽器

function callbackHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.getPhoto(user.id).then(function (photo) {
        console.log('callbackHell', { user, friends, photo })
      })
    })
  })
}
複製代碼

對於任何一個從事過JavaScript項目開發的人來講,這個代碼塊很是熟悉。很是簡單的業務邏輯,可是代碼倒是冗長、深嵌套,而且以這個結尾.....bash

})
    })
  })
}
複製代碼

在真實的業務場景中,每一個回調函數可能更復雜,代碼塊會以一堆充滿層次感的})爲結尾。「回調函數裏面嵌套着回調函數嵌套着回調函數」,這就是被傳說中的「回調地獄」(「回調地獄」的誕生不僅是由於代碼塊的混亂,也源於信任問題。)。 更糟糕的是,咱們爲了簡化,尚未作錯誤處理機制,若是加上了reject......細思極恐

方法二 --- Promise Chain

讓咱們優雅起來

function promiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('promiseChain', { user, friends, photo })
    })
}
複製代碼

Promise有一個很棒的特性:Promise.prototype.then()和Promise.prototype.catch()返回Promise對象,這就使得咱們能夠將這些promise鏈接成一個promise鏈。經過這種方法,咱們能夠將這些回調函數放在一個縮進層次裏。與此同時,咱們使用了箭頭函數簡化了回調函數聲明。 對比以前的回調地獄,使用promise鏈使得代碼的可讀性大大提升而且擁有着更好的序列感,可是看起來仍是很是冗長而且有一點複雜。

方法三 --- Async/Await

咱們可不能夠不寫回調函數?就寫7行代碼能解決嗎?

async function asyncAwaitIsYourNewBestFriend () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  console.log('asyncAwaitIsYourNewBestFriend', { user, friends, photo })
}
複製代碼

優雅多了,調用await以前咱們會一直等待,直到promise被決議並將值賦值給左邊的變量。經過async/await,咱們能夠對異步操做流程進行控制,就好像它是同步代碼。

注:await必須搭配async一塊兒使用,注意上面的函數,咱們將關鍵字async放在了函數的聲明前,這是必需的。稍後,咱們會深刻討論這個問題

循環

Async/Await可讓之前不少複雜的代碼變得簡明。舉個例子,若是咱們要按序檢索每一個用戶的朋友的朋友列表。

方法一 --- Recursive Promise Loop

下面是使用傳統的promise來按序獲取每一個朋友的朋友列表

function promiseLoops () {  
  const api = new Api()
  api.getUser()
    .then((user) => {
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      const getFriendsOfFriends = (friends) => {
        if (friends.length > 0) {
          let friend = friends.pop()
          return api.getFriends(friend.id)
            .then((moreFriends) => {
              console.log('promiseLoops', moreFriends)
              return getFriendsOfFriends(friends)
            })
        }
      }
      return getFriendsOfFriends(returnedFriends)
    })
}
複製代碼

咱們建立在promiseLoops中建立了一個函數用於遞歸地去獲取朋友的朋友列表。這個函數體現了函數式編程,可是對於這個簡單的任務而言,這依舊是一個比較複雜的解決方案。

方法二 --- Async/Await For-Loop

讓咱們嘗試一下Async/Await

async function asyncAwaitLoops () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)

  for (let friend of friends) {
    let moreFriends = await api.getFriends(friend.id)
    console.log('asyncAwaitLoops', moreFriends)
  }
}
複製代碼

不須要寫遞歸promise閉包,只須要使用一個for循環就能解決咱們的問題。

並行

一個一個地去獲取朋友的朋友的列表看起來有點慢,爲何不併行處理請求呢?咱們能夠用async/await來處理並行任務嗎? 固然

async function asyncAwaitLoopsParallel () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const friendPromises = friends.map(friend => api.getFriends(friend.id))
  const moreFriends = await Promise.all(friendPromises)
  console.log('asyncAwaitLoopsParallel', moreFriends)
}
複製代碼

爲了並行請求,咱們使用了一個promise數組並將它傳遞給方法Promise.all(),Promise.all()會返回一個promise,一旦全部的請求完成就會決議。

錯誤處理

然而,在異步編程中有一個主要的問題還沒解決:錯誤處理。在異步操做中,咱們必須爲每一個操做編寫單獨的錯誤處理回調,在調用棧的頂部去找出正確的報錯位置可能很複雜,因此咱們得在每一個回調開始時就去檢查是否拋出了錯誤。因此,引入錯誤處理後的回調函數會比以前複雜度成倍增長,若是沒有主動定位到報錯的位置,這些錯誤甚至會被「吞掉」。 如今,咱們給以前的例子添上錯誤處理機制。爲了測試錯誤處理機制,咱們將在真正獲取到用戶圖片以前使用抽象類裏的api.throwError()方法。

方法一 --- Promise Error Callbacks

讓咱們看看最壞的狀況

function callbackErrorHell () {
  const api = new Api()
  let user, friends
  api.getUser().then(function (returnedUser) {
    user = returnedUser
    api.getFriends(user.id).then(function (returnedFriends) {
      friends = returnedFriends
      api.throwError().then(function () {
        console.log('Error was not thrown')
        api.getPhoto(user.id).then(function (photo) {
          console.log('callbackErrorHell', { user, friends, photo })
        }, function (err) {
          console.error(err)
        })
      }, function (err) {
        console.error(err)
      })
    }, function (err) {
      console.error(err)
    })
  }, function (err) {
    console.error(err)
  })
}
複製代碼

代碼除了又長又醜陋之外,代碼操做流也不直觀,不像同步、可讀性高的代碼那樣從上往下。

方法二 --- Promise Chain "Catch" Method

咱們能夠給promise鏈添加catch方法來改善一些

function callbackErrorPromiseChain () {
  const api = new Api()
  let user, friends
  api.getUser()
    .then((returnedUser) => {
      user = returnedUser
      return api.getFriends(user.id)
    })
    .then((returnedFriends) => {
      friends = returnedFriends
      return api.throwError()
    })
    .then(() => {
      console.log('Error was not thrown')
      return api.getPhoto(user.id)
    })
    .then((photo) => {
      console.log('callbackErrorPromiseChain', { user, friends, photo })
    })
    .catch((err) => {
      console.error(err)
    })
}
複製代碼

看起來好多了,咱們經過給promise添加一個錯誤處理取代了以前給每一個回調函數添加錯誤處理。可是,這仍是有一點複雜而且咱們仍是須要使用一個特殊的回調來處理異步錯誤而不是像對待正常的JavaScript錯誤那樣處理它們。

方法三 --- Normal Try/Catch Block

咱們能夠作得更好

async function aysncAwaitTryCatch () {
  try {
    const api = new Api()
    const user = await api.getUser()
    const friends = await api.getFriends(user.id)

    await api.throwError()
    console.log('Error was not thrown')

    const photo = await api.getPhoto(user.id)
    console.log('async/await', { user, friends, photo })
  } catch (err) {
    console.error(err)
  }
}
複製代碼

咱們將異步操做放進了處理同步代碼的try/catch代碼塊。經過這種方法,咱們徹底能夠像對待同步代碼的同樣處理異步代碼的錯誤。代碼看起來很是簡明

組合

我在前面說起了任何以async的函數能夠返回一個promise。這使得咱們能夠真正輕鬆地組合異步控制流 舉個例子,咱們能夠從新整理前面的例子,將獲取數據和處理數據分開。這樣咱們就能夠經過調用async函數獲取數據。

async function getUserInfo () {
  const api = new Api()
  const user = await api.getUser()
  const friends = await api.getFriends(user.id)
  const photo = await api.getPhoto(user.id)
  return { user, friends, photo }
}

function promiseUserInfo () {
  getUserInfo().then(({ user, friends, photo }) => {
    console.log('promiseUserInfo', { user, friends, photo })
  })
}
複製代碼

更棒的是,咱們能夠在數據接受函數裏使用async/await,這將使得整個異步模塊更加明顯。 若是咱們要獲取前面10個用戶的數據呢?

async function getLotsOfUserData () {
  const users = []
  while (users.length < 10) {
    users.push(await getUserInfo())
  }
  console.log('getLotsOfUserData', users)
}
複製代碼

併發呢?而且加上錯誤處理呢?

async function getLotsOfUserDataFaster () {
  try {
    const userPromises = Array(10).fill(getUserInfo())
    const users = await Promise.all(userPromises)
    console.log('getLotsOfUserDataFaster', users)
  } catch (err) {
    console.error(err)
  }
}
複製代碼

結論

隨着SPA的興起和NodeJS的普遍應用,對於JavaScript開發人員來講,優雅地處理併發性比以往任什麼時候候都要重要。Async/Await緩解了許多由於bug引發且已經影響JavaScript不少年的控制流問題,而且使得代碼更加優雅。現在,主流的瀏覽器和NodeJS都已經支持了這些語法糖,因此如今是使用Async/Await的最好時機。

相關文章
相關標籤/搜索