異步JavaScript的演化史:從回調到Promise再到Async/Await

做者|Tyler McGinnis
譯者|張衛濱javascript

本文以實際樣例闡述了異步 JavaScript 的發展過程,介紹了每種實現方式的優點和不足,可以幫助讀者掌握相關技術的使用方式並把握技術發展的脈絡。java

我最喜歡的一個站點叫作 BerkshireHathaway.com,它很是簡單、高效,從 1997 年建立以來它一直都能很好地完成本身的任務。尤爲值得注意的是,在過去的 20 年間,這個站點歷來沒有出現過缺陷。這是爲何呢?由於它是靜態的,從創建到如今的 20 年間,它幾乎沒有發生過什麼變化。若是你將全部的數據都放在前面的話,搭建站點是很是簡單的。可是,現在大多數的站點都不會這麼作。爲了彌補這一點,咱們發明了所謂的「模式」,幫助咱們的應用從外部抓取數據。同其餘大多數事情同樣,這些模式都有必定的權衡,並隨着時間的推移在發生着變化。在本文中,咱們將會分析三種經常使用模式的優劣,即回調(Callback)、Promise 和 Async/Await,並從歷史發展的維度討論一下它們的意義和發展。git

咱們首先從數據獲取模式的最原始方式開始介紹,那就是回調。github

回調

在這裏我假設你對回調一無所知,若是事實並不是如此的話,那麼你能夠將內容稍微日後拖動一下。編程

當我第一次學習編程的時候,它就幫助我造成了一種思考方式,那就是將功能視爲一種機器。這些機器可以完成任何你但願它能作到的事情,它們甚至可以接受輸入並返回值。每一個機器都有一個按鈕,若是你但願這個機器運行的話,就按下按鈕,這個按鈕也就是 ()。json

function add (x, y) {
  return x + y
}

add(2,3) // 5 - 按下按鈕,運行機器。

事實上,不只我能按下按鈕,你 也能夠,任何人 按下按鈕的效果都是同樣的。只要按下按鈕,無論你是否願意,這個機器就會開始運行。api

function add (x, y) {
 return x + y
}

const me = add
const you = add
const someoneElse = add

me(2,3) // 5 - 按下按鈕,運行機器。
you(2,3) // 5 - 按下按鈕,運行機器。
someoneElse(2,3) // 5 - 按下按鈕,運行機器。

在上面的代碼中,咱們將add函數賦值給了三個不一樣的變量:me、you和someoneElse。有很重要的一點須要注意,原始的add和咱們建立的每一個變量都指向的相同的內存點。在不一樣的名字之下,它們其實是徹底相同的內容。因此,當咱們調用me、you或someoneElse的時候,就像調用add同樣。數組

若是咱們將add傳遞給另一臺機器又會怎樣呢?須要記住,無論誰按下這個「()」按鈕,它都會執行。promise

function add (x, y) {
  return x + y
}

function addFive (x, addReference) {
  return addReference(x, 5) // 15 - 按下按鈕,運行機器。
}

addFive(10, add) // 15

你可能會以爲這有些詭異,可是這裏沒有任何新東西。此時,咱們再也不是在add上「按下按鈕」,而是將add做爲參數傳遞給addFive,將其重命名爲addReference,而後咱們「按下按鈕」或者說調用它。安全

這裏涉及到了 JavaScript 的一些重要概念。首先,就像能夠將字符串或數字以參數的形式傳遞給函數同樣,咱們還能夠將函數的引用做爲參數進行傳遞。但咱們這樣作的時候,做爲參數傳遞的函數被稱爲回調函數(callback function),而接收回調函數傳入的那個函數則被稱爲高階函數(higher order function)。

由於術語很是重要,因此對相同功能的代碼,咱們進行變量的重命名,使其匹配它們所要闡述的概念:

function add (x,y) {
  return x + y
}

function higherOrderFunction (x, callback) {
  return callback(x, 5)
}

higherOrderFunction(10, add)

這種模式看上去應該是很是熟悉的,它處處可見。若是你曾經用過 JavaScript 的 Array 方法,那麼你所使用的就是回調。若是你用過 lodash,那麼你所使用的就是回調。若是你用過 jQuery,那麼你所使用的也是回調。

[1,2,3].map((i) => i + 5)

_.filter([1,2,3,4], (n) => n % 2 === 0 );

$('#btn').on('click', () =>
  console.log('Callbacks are everywhere')
)

通常而言,回調有兩種常見的使用場景。首先,也就是咱們在.map和 _.filter樣例中所看到的,對於從一個值轉換成另外一個值的場景,這是一種很是好的抽象。咱們能夠說「這裏有一個數組和一個函數。基於我給你的函數獲得一個新的值」。其次,也就是咱們在 jQuery 樣例中所看到的,將函數的執行延遲至一個特定的時間。「這裏有一個函數,當 id 爲btn的元素被點擊時,執行這個函數」。咱們接下來會主要關注第二個使用場景,「將函數的執行延遲至一個特定的時間」。

如今,咱們只看到了同步操做的樣例。正如咱們在本文開始時提到的那樣,咱們所構建的大多數應用都不會將數據預先準備好,而是用戶在與應用進行交互時,按需抓取外部的數據。經過上面的介紹,咱們很快就能判斷得出這個場景很是適合使用回調,由於它容許咱們「將函數的執行延遲至一個特定的時間」。咱們可以瓜熟蒂落的將這句話應用到數據獲取的情景中。此時再也不是將函數的執行延遲到一個特定的時間,而是將函數的執行延遲至咱們獲得了想要的數據以後。jQuery 的getJSON方法多是這種模式最多見的樣例:

// updateUI 和 showError 的內容可有可無。
// 假定它們所作的工做與它們的名字相同。

const id = 'tylermcginnis'

$.getJSON({
  url: `https://api.github.com/users/${id}`,
  success: updateUI,
  error: showError,
})

在獲取到用戶的數據以前,咱們是不能更新應用的 UI 的。那麼咱們是怎麼作的呢?咱們能夠說,「這是一個對象。若是請求成功的話,那麼調用success,並將用戶的數據傳遞給它。若是請求沒有成功的話,那麼調用error並將錯誤對象傳遞給它。你不用關心每一個方法是作什麼的,只須要確保在應該調用它們的時候,去進行調用就能夠了。這個樣例完美地闡述瞭如何使用回調進行異步請求。

到此爲止,咱們已經學習了回調是什麼以及它如何爲同步代碼和異步代碼帶來收益。咱們尚未討論回調的陰暗面。看一下下面的代碼,你能告訴我它都作了些什麼嗎?

// updateUI、showError 和 getLocationURL 的內容可有可無。
// 假定它們所作的工做與它們的名字相同。

const id = 'tylermcginnis'

$("#btn").on("click", () => {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: (user) => {
      $.getJSON({
        url: getLocationURL(user.location.split(',')),
        success (weather) {
          updateUI({
            user,
            weather: weather.query.results
          })
        },
        error: showError,
      })
    },
    error: showError
  })
})

[若是你須要幫助的話,能夠參考一下這些代碼的在線版本] (https://codesandbox.io/s/v06mmo3j7l)。

注意一下,咱們只是多添加了幾層回調。首先,咱們仍是聲明,若是不點擊 id 爲btn的元素,那麼原始的 AJAX 請求就不會發送。一旦點擊了按鈕,咱們會發起第一個請求。若是請求成功的話,咱們會發起第二個請求。若是第二個請求也成功的話,那麼咱們將會調用updateUI方法,並將兩個請求獲得的數據傳遞給它。無論你一眼是否可以明白這些代碼,客觀地說,它要比以前的代碼更加難以閱讀。這也就涉及到所謂的「回調地獄」。

做爲人類,咱們習慣於序列化的思考方式。若是在嵌套回調中依然還有嵌套回調的話,它會強迫咱們背離天然的思考方式。當代碼的閱讀方式與你的思考方式脫節時,缺陷也就難以免地出現了。

與大多數軟件問題的解決方案相似,簡化「回調地獄」問題的一個常見方式就是對你的代碼進行模塊化。

function getUser(id, onSuccess, onFailure) {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: onSuccess,
    error: onFailure
  })
}

function getWeather(user, onSuccess, onFailure) {
  $.getJSON({
    url: getLocationURL(user.location.split(',')),
    success: onSuccess,
    error: onFailure,
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis", (user) => {
    getWeather(user, (weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    }, showError)
  }, showError)
})

[若是你須要幫助的話,能夠參考一下這些代碼的在線版本] (https://codesandbox.io/s/m587rq0lox)。

好了,函數的名稱可以幫助咱們理解到底會發生什麼,但客觀地說,它真的「更好」了嗎?其實也沒好到哪裏去。咱們只是給回調地獄這個問題上添加了一塊創可貼。問題依然存在,也就是咱們會天然地按照順序進行思考,即使有了額外的函數,嵌套回調也會打斷咱們順序思考的方式。

回調方式的另一個問題與 控制反轉(inversion of control) 有關。當你編寫回調的時候,你會假設本身將回調交給了一個負責任的程序,這個程序會在(而且僅會在)應該調用的時候調用你的回調。你實際上將控制權交給了另一個程序。當你在處理 jQuery、lodash 這樣的庫,甚至普通 JavaScript 時,能夠安全地假設回調函數會在正確的時間以正確的參數進行調用。可是,對於不少第三方庫來講,回調函數是你與它們進行交互的接口。第三方庫極可能有意或無心地破壞與你的回調進行交互的方式。

function criticalFunction () {
  // 這個函數必定要進行調用,而且要傳入正確的參數
}

thirdPartyLib(criticalFunction)

由於你不是調用criticalFunction的人,所以你徹底沒法控制它什麼時候被調用以及使用什麼參數進行調用。大多數狀況下,這都不是什麼問題,可是一旦出現問題的話,就不是什麼小問題。

Promise

你有沒有不預定就進入一家繁忙餐廳的經歷?在這種狀況下,餐廳須要有一種方式在出現空桌時可以聯繫到你。過去,他們只會把你的名字記錄下來並在出現空桌的時候呼喊你的名字。隨後,他們天然而然地尋找更有意思的方案。有一種方式是他們再也不記錄你的名字,而是記錄你的電話號碼,當出現空桌的時候,他們就能夠爲你發送短信。這樣一來,你就能夠離開最初的呼喊範圍了,可是更重要的是,這種方式容許他們在任什麼時候候給你的電話發送廣告。聽起來很熟悉吧?應該是這樣的!固然也可能並不是如此。這種方式能夠用來類比回調。將你的電話號碼告訴餐廳就像將你的回調函數交給第三方服務同樣。你指望餐廳在有空桌的時候給你發送短信,一樣咱們也指望第三方服務在合適的時候以它們承諾的方式調用咱們函數。可是,一旦電話號碼或回調函數交到了他們的手裏,咱們就徹底失去對它的控制了。

幸虧,還有另一種解決方案。這種方案的設計容許你保留全部的控制權。你可能以前見過這種方式,那就是他們會給你一個蜂鳴器,以下所示。

若是你以前沒有用過的話,它的想法其實很是簡單。按照這種方式,他們不會記錄你的名字或電話號碼,而是給你一個這樣的設備。當這個設備開始嗡嗡做響和發光時,就意味着有空桌了。在等待空桌的時候,你能夠作任何你想作的事情,但此時你不須要放棄任何的東西。實際上,偏偏相反,是 他們 須要給 你 東西,這裏沒有所謂的控制反轉。

蜂鳴器必定會處於以下三種狀態之一:pending、fulfilled或rejected。

pending:默認狀態,也是初始態。當他們給你蜂鳴器的時候,它就是這種狀態。

fulfilled:表明蜂鳴器開始閃爍,你的桌子已經準備就緒。

rejected:若是蜂鳴器處於這種狀態,則表明出現了問題。可能餐廳要打烊,或者他們忘記了晚上有人要包場。

再次強調,你做爲蜂鳴器的接收者擁有徹底的控制權。若是蜂鳴器處於fulfilled狀態,你就能夠就坐了。若是它進入fulfilled狀態,可是你想忽略它,一樣也能夠。若是它進入了rejected狀態,這很是糟糕,可是你能夠選擇去其餘地方就餐。若是什麼事情都沒有發生,它會依然處於pending狀態,你可能吃不上飯了,可是同時也沒有失去什麼。

如今,你已經掌握了餐廳蜂鳴器的事情,接下來,咱們將這個知識用到其餘重要的地方。

若是說將電話號碼告訴餐廳就像將回調函數交給他們同樣的話,那麼接受這個蜂鳴器就像咱們所謂的「Promise」同樣。

像以往同樣,咱們首先從 爲何 開始。Promise 爲何會存在呢?它的出現是爲了讓異步請求所帶來的複雜性更容易管理。與蜂鳴器很是相似,Promise會處於以下三種狀態中的某一種: pending、fulfilled或rejected。可是與蜂鳴器不一樣,這些狀態表明的不是飯桌的狀態,它們所表明的是異步請求的狀態。

若是異步請求依然還在進行,那麼Promise的狀態會是pending。若是異步請求成功完成的話,那麼Promise會將狀態轉換爲fulfilled。若是異步請求失敗的話,Promise會將狀態轉換爲rejected。蜂鳴器的比喻很是貼切,對吧?

理解了 Promise 爲何會存在以及它們的三種不一樣狀態以後,咱們還要回答三個問題:

如何建立 Promise?

如何改變 Promise 的狀態?

如何監聽 Promise 狀態變化的時間?

#### 1)如何建立 Promise?
這個問題很是簡單,你可使用new建立Promise的一個實例:

const promise = new Promise()

####2)如何改變 Promise 的狀態?
Promise的構造函數會接收一個參數,這個參數是一個(回調)函數。該函數會被傳入兩個參數resolve和reject。

resolve:一個能將 Promise 狀態變爲fulfilled的函數;

reject:一個能將 Promise 狀態變爲rejected的函數;

在下面的代碼中,咱們使用setTimeout等待兩秒鐘而後調用resolve,這樣會將 Promise 的狀態變爲fulfilled:

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve() // 將狀態變爲「fulfilled」
  }, 2000)
})

分別用日誌記錄剛剛建立之時和大約兩秒鐘以後resolve已被調用時的 Promise,咱們能夠看到狀態的變化了:

<iframe height=500 width=800 src="https://wx4.sinaimg.cn/mw690/72776b8cgy1fx3ekzi7scg20vs0k2nph.gif"></iframe>

請注意,Promise 從 變成了

3)如何監聽 Promise 狀態變化的時間?

我認爲,這是最重要的一個問題。咱們已經知道了如何建立 Promise 和改變它的狀態,這很是棒,可是若是咱們不知道如何在狀態變化以後作一些事情的話,這實際上是沒有太大意義的。

咱們尚未討論的一件事就是 Promise 究竟是什麼。當咱們經過new Promise建立 Promise 的時候,你實際建立的只是一個簡單的 JavaScript 對象,這個對象能夠調用兩個方法then和catch。這是關鍵所在,當 Promise 的狀態變爲fulfilled的時候,傳遞給.then的函數將會被調用。若是 Promise 的狀態變爲rejected,傳遞給.catch的函數將會被調用。這就意味着,在你建立 Promise 的時候,要經過.then將你但願異步請求成功時調用的函數傳遞進來,經過.catch將你但願異步請求失敗時調用的函數傳遞進來。

看一下下面的樣例。咱們依然使用setTimeout在兩秒鐘(2000 毫秒)以後將 Promise 的狀態變爲fulfilled:

function onSuccess () {
  console.log('Success!')
}

function onError () {
  console.log('💩')
}

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 2000)
})

promise.then(onSuccess)
promise.catch(onError)

若是你運行上述代碼,會發現大約兩秒鐘以後,將會在控制檯上打印出「Success!」。出現這樣的結果主要有兩個緣由。首先,當咱們建立 Promise 的時候,會在大約 2000 毫秒以後調用resolve,這會將 Promise 的狀態變爲fulfilled。其次,咱們將onSuccess函數傳遞給 Promise 的.then。經過這種方式,咱們告訴 Promise 在狀態變成fulfilled的時候(也就是大約 2000 毫秒以後)調用onSuccess。

如今,咱們假設發生了意料以外的事情,須要將 Promise 的狀態變成rejected。此次,咱們再也不調用resolve,而是應該調用reject:

function onSuccess () {
  console.log('Success!')
}

function onError () {
  console.log('💩')
}

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject()
  }, 2000)
})

promise.then(onSuccess)
promise.catch(onError)

這一次,調用的就不是onSuccess函數了,而是onError函數,這是由於咱們調用了reject。

如今,你已經掌握了 Promise API 相關的知識,如今咱們開始看一下真正的代碼。

還記得咱們以前看到的異步回調樣例嗎?

function getUser(id, onSuccess, onFailure) {
  $.getJSON({
    url: `https://api.github.com/users/${id}`,
    success: onSuccess,
    error: onFailure
  })
}

function getWeather(user, onSuccess, onFailure) {
  $.getJSON({
    url: getLocationURL(user.location.split(',')),
    success: onSuccess,
    error: onFailure,
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis", (user) => {
    getWeather(user, (weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    }, showError)
  }, showError)
})

這裏咱們能用 Promise API 的方式替換回調嗎?若是咱們將 AJAX 請求包裝到 Promise 中會怎麼樣呢?若是能這樣的話,咱們就能夠根據請求執行的狀況簡單地調用resolve或reject。讓咱們從getUser入手:

function getUser(id) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: `https://api.github.com/users/${id}`,
      success: resolve,
      error: reject
    })
  })
}

很是好!請注意,getUser的參數發生了變化。從接收id、onSuccess和onFailure變成了只接收id。這裏再也不須要這兩個回調函數了,由於咱們沒必要再將控制權轉移出去了。相反,咱們在這裏使用了 Promise 的resolve和reject函數。若是請求成功的話,將會調用resolve,若是出現錯誤的話,將會調用reject。

接下來,咱們重構getWeather。咱們按照相同的策略,將onSuccess和onFailure回調函數替換爲resolve和reject。

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(',')),
      success: resolve,
      error: reject,
    })
  })
}

看上去很是不錯!咱們須要更新的最後一個地方就是點擊處理器。須要記住,咱們想要的處理流程以下所示:

經過 Github API 獲取用戶的信息;

使用用戶的地理位置,經過 Yahoo Weather API 獲取其天氣狀況;

根據用戶信息和天氣信息更新 UI。

咱們從第一步開始:經過 Github API 獲取用戶的信息。

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

  userPromise.then((user) => {

  })

  userPromise.catch(showError)
})

注意,getUser再也不接收兩個回調函數,它爲咱們返回的是一個 Promise,基於該 Promise,咱們能夠調用.then和.catch。若是調用.then的話,會將用戶信息傳遞給它。若是調用.catch的話,會將錯誤信息傳遞給它。

接下來,讓咱們實現第二步:使用用戶的地理位置獲取其天氣。

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

  userPromise.then((user) => {
    const weatherPromise = getWeather(user)
    weatherPromise.then((weather) => {

    })

    weatherPromise.catch(showError)
  })

  userPromise.catch(showError)
})

注意,咱們採起了與第一步徹底相同的模式,只不過調用getWeather的時候,咱們將userPromise獲得的user傳遞了進去。

最後,在第三步中咱們使用用戶信息及其天氣信息更新 UI。

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')

  userPromise.then((user) => {
    const weatherPromise = getWeather(user)
    weatherPromise.then((weather) => {
      updateUI({
        user,
        weather: weather.query.results
      })
    })

    weatherPromise.catch(showError)
  })

  userPromise.catch(showError)
})

在該 地址 有完整的源碼,你能夠進行嘗試。

咱們的新代碼已經好多了,可是依然能夠作一些改善。可是在進行改善以前,你須要瞭解 Promise 的另外兩個特性,那就是連接(chaining)以及從resolve中給then傳遞參數。

連接

.then和.catch都會返回一個新的 Promise。這看上去像是一個很小的細節,但實際上是很是重要的,由於這意味着 Promise 可以連接起來。

在下面的樣例中,咱們調用getPromise,它會返回一個 Promise,這個 Promise 會在 2000 毫秒以後進行resolve。從這裏開始,由於.then也將返回一個 Promise,因此咱們就能夠將多個.then連接起來,直到咱們拋出一個new Error,而這個錯誤將會被.catch方法捕獲。

function getPromise () {
  return new Promise((resolve) => {
    setTimeout(resolve, 2000)
  })
}

function logA () {
  console.log('A')
}

function logB () {
  console.log('B')
}

function logCAndThrow () {
  console.log('C')

  throw new Error()
}

function catchError () {
  console.log('Error!')
}

getPromise()
  .then(logA) // A
  .then(logB) // B
  .then(logCAndThrow) // C
  .catch(catchError) // Error!

這樣很是酷,但爲何它如此重要呢?還記得在討論回調的章節中,咱們討論了回調的劣勢之一就是它強迫咱們背離天然、序列化的思考方式。當咱們將 Promise 連接起來的時候,它不會再強迫咱們背離天然的思考方式,由於連接以後的 Promise 是序列化的,也就是運行getPromise,而後運行logA,而後運行logB……

咱們看另一個樣例,這是使用fetch API 時很常見的場景。fetch將會爲咱們返回一個 Promise,它會解析爲 HTTP 響應。爲了獲得實際的 JSON,咱們還須要調用.json。由於這種連接的方式,咱們能夠按照序列化的方式進行思考:

fetch('/api/user.json')
  .then((response) => response.json())
  .then((user) => {
    // user 如今已經準備就緒了。
  })

如今咱們已經明白了連接的方式,接下來咱們使用它來重構以前使用的getUser/getWeather代碼。

function getUser(id) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: `https://api.github.com/users/${id}`,
      success: resolve,
      error: reject
    })
  })
}

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(',')),
      success: resolve,
      error: reject,
    })
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((weather) => {
      // 在這裏咱們同時須要 user 和 weather
      // 目前咱們只有 weather
      updateUI() // ????
    })
    .catch(showError)
})

這看起來好多了,可是如今咱們遇到了另一個問題。你發現了嗎?在第二個.then中,咱們想要調用updateUI。這裏的問題在於咱們須要爲updateUI同時傳遞user和weather。按照咱們目前的作法,咱們只能接收到weather,而沒有user。咱們須要想出一種辦法,讓getWeather在resolve時可以同時獲得user和weather。

問題的關鍵在於resolve只是一個函數。你傳遞給它的任何參數都會往下傳遞給.then所指定的函數。這意味着,在getWeather中,若是咱們自行調用resolve的話,就能夠同時將weather和user。而後,在鏈中的第二個.then方法中,就能夠同時接收到weather和user。

function getWeather(user) {
  return new Promise((resolve, reject) => {
    $.getJSON({
      url: getLocationURL(user.location.split(',')),
      success(weather) {
        resolve({ user, weather: weather.query.results })
      },
      error: reject,
    })
  })
}

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((data) => {
      // Now, data is an object with a
      // "weather" property and a "user" property.

      updateUI(data)
    })
    .catch(showError)
})

你能夠在該 地址 查看最後的代碼。

在點擊處理邏輯中,與回調方式進行對比,咱們就能看出 Promise 的威力。

// Callbacks 🚫
getUser("tylermcginnis", (user) => {
  getWeather(user, (weather) => {
    updateUI({
      user,
      weather: weather.query.results
    })
  }, showError)
}, showError)


// Promises ✅
getUser("tylermcginnis")
  .then(getWeather)
  .then((data) => updateUI(data))
  .catch(showError);

此時邏輯感受很是天然,由於它就是咱們所習慣的序列化思考方式。getUser,而後getWeather,而後使用獲得的數據更新 UI。

顯而易見,Promise 可以顯著提高異步代碼的可讀性,可是有沒有能讓它更好的方式呢?假設你是 TC39 委員會的成員,擁有爲 JavaScript 語言添加新特性的權力。那麼,你認爲下面的代碼還能怎樣進行優化?

$("#btn").on("click", () => {
  getUser("tylermcginnis")
    .then(getWeather)
    .then((data) => updateUI(data))
    .catch(showError)
})

正如咱們在前面所討論的,這個代碼已經很是好了。與咱們大腦的思考方式相同,它是序列化順序的。咱們所遇到的問題就是須要將數據(users)從第一個異步請求一直傳遞到最後一個.then。這並非什麼大問題,可是須要咱們修改getWeather才能往下傳遞users。若是咱們想徹底按照編寫同步代碼的方式來編寫異步代碼會怎樣進行處理呢?若是咱們真的能作到這一點,這個問題將會完全消失,它看上去就徹底是序列化的了。以下是可能的一種實現方式:

$("#btn").on("click", () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)

  updateUI({
    user,
    weather,
  })
})

這看上去很是棒,咱們的異步代碼與同步代碼徹底相同。咱們的大腦無需任何額外的步驟,由於這就是咱們已經習覺得常的思考方式。但使人遺憾的是,這樣顯然沒法正常運行。咱們都知道,user和weather僅僅是getUser和getWeather所返回的 Promise。可是不要忘記,咱們如今是 TC39 的成員,有爲語言添加任何特性的權力。這樣的代碼很難運行,咱們必須教會 JavaScript 引擎區分異步函數調用和常規同步函數調用以前的差別。咱們接下來添加幾個關鍵字,讓引擎運行起來更加容易。

首先,咱們添加一個關鍵字到主函數上。這會提示引擎,咱們會在這個函數中添加一些異步的方法調用。咱們使用async來達到這一目的。

$("#btn").on("click", async () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)

  updateUI({
    user,
    weather,
  })
})

很好!這看上去是很是合理的。接下來,咱們添加另一個關鍵字,讓引擎可以知道哪一個函數調用是異步的,函數所返回的是 Promise。這裏咱們使用await,這就至關於說「嗨,引擎。這個函數是異步的而且會返回 Promise。你不能按照慣常的方式來執行,你須要等待 Promise 的最終值,而後才能繼續運行」。在新的async和await就緒以後,代碼將會變成下面的樣子。

$("#btn").on("click", async () => {
  const user = await getUser('tylermcginnis')
  const weather = await getWeather(user.location)

  updateUI({
    user,
    weather,
  })
})

這種方式至關有吸引力。咱們有了一種合理的方式,讓異步代碼的外表和行爲徹底和同步代碼一致。接下來,就該讓 TC39 的人相信這是一個好辦法。你可能已經猜到了,咱們並不須要說服他們,由於這已是 JavaScript 的組成部分之一了,也就是所謂的Async/Await。

你還不相信嗎?[該地址] (https://codesandbox.io/s/00w10o19xn) 展示了添加 Async/Await 以後的實際代碼,你盡能夠進行嘗試。

異步函數會返回 Promise

如今,咱們已經看到了 Async/Await 所能帶來的收益。接下來,咱們討論幾個更小的細節,掌握它們也是很是重要的。首先,只要你爲函數添加async,它就會隱式的返回一個 Promise:

async function getPromise(){}

const promise = getPromise()

儘管getPromise實際上空的,可是它依然會返回一個 Promise,由於它是一個async函數。

若是async函數有返回值的話,它也將會包裝到一個 Promise 中。這意味着,你必需要使用.then來訪問它。

async function add (x, y) {
  return x + y
}

add(2,3).then((result) => {
  console.log(result) // 5
})

不能將 await 用到非 async 的函數中

若是你將await用到非async的函數中,那麼將會出現錯誤。

$("#btn").on("click", () => {
  const user = await getUser('tylermcginnis') // SyntaxError: await is a reserved word
  const weather = await getWeather(user.location) // SyntaxError: await is a reserved word

  updateUI({
    user,
    weather,
  })
})

關於這一點,我認爲,當你將async添加到一個函數上的時候,它會作兩件事,首先它會讓這個函數自己返回一個 Promise(或者將返回的內容包裝到 Promise 中),其次,它會確保你可以在這個函數中使用await。

錯誤處理

你可能發現,咱們的代碼有一點做弊。在原始的代碼中,咱們能夠經過.catch捕獲全部的錯誤。在切換到 Async/Await 以後,咱們移除了那些代碼。在使用 Async/Await 時,最經常使用的方式就是將你的代碼包裝到一個try/catch中,這樣就能捕獲錯誤了。

$("#btn").on("click", async () => {
  try {
    const user = await getUser('tylermcginnis')
    const weather = await getWeather(user.location)

    updateUI({
      user,
      weather,
    })
  } catch (e) {
    showError(e)
  }
})

轉載
原文連接

相關文章
相關標籤/搜索