[譯] JavaScript 異步演進史,從 Callbacks, Promises 到 Async/Await

原文連接 The Evolution of Async JavaScript: From Callbacks, to Promises, to Async/Awaitjavascript

注:本文爲個人課程《高級 JavaScript》中的一部分,若是你喜歡本文,歡迎你來看看個人課程。java

BerkshireHathaway.com 是我最喜歡的網站之一,由於它簡單、高效,並且自從 1997 年建立以來一直運行良好。更🐂🍺的是在過去的二十年裏這個網站頗有可能從未出現過 bug。git

爲啥?由於這個網站是純靜態的,它自20多年前推出以來幾乎從沒變過樣子。github

也就是說若是你預先擁有全部數據,那麼建站就會變得很是簡單。不幸的是,現現在的大多數站點都不是這樣的。爲了彌補這方面的缺點,咱們爲咱們的系統發明了各類「模式」來應對這種須要獲取外部數據的狀況。編程

與其餘事物同樣,這些模式隨着時間的推移都會權衡各自不一樣的側重點。本文將詳細拆解Callbacks, Promises, 和Async/Await這三種最多見模式的優缺點,同時在其歷史背景下探討一下這種演進產生的意義及進步之處。json

Callbacks

這裏會假設你徹底不瞭解何爲 callbacks。要是我假設有誤,稍微向下滑一下便可。api

在我剛開始學習編程的時候,我把函數當作一臺機器。這些機器能夠作任何你想讓他們完成的工做。甚至是接收一個輸入而後返回一個值。每臺機器都有一個按鈕,你能夠在你想讓他們運轉的時候按下這個按鈕,在函數裏,也就是()數組

function add (x, y) {
    return x + y;
}
add(2,3)
複製代碼

這個按鈕是什麼時候被誰按下的並不重要,機器只管去運行。promise

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賦值給了不一樣的變量meyousomeoneElse,值得注意的是,原函數add和咱們定義的三個變量都指向的是同一塊內存。實際上它們是同一個東西,只不過有不一樣的名字。所以當咱們調用meyousomeoneElse時,就好像咱們在調用add同樣。異步

如今,要是咱們把add這臺機器傳入到其餘機器中去會發生什麼?記住,誰按下了()按鈕並不重要,重要的是隻要按鈕被按下,機器就會運行起來。

function add (x, y) {
  return x + y
}
function addFive (x, addReference) {
  return addReference(x, 5) // 15 - 按下按鈕,啓動機器
}
addFive(10, add) // 15
複製代碼

第一眼看到這個代碼的時候你可能會感受有點兒奇怪,實際上並無啥新東西在裏面。咱們並無直接按下函數add的啓動按鈕,而是將函數add做爲參數傳給了函數addFive,把add重命名爲addReference,而後咱們「按下按鈕」,或者說是調用了它。

這就引出了 JavaScript 中一個比較重要的概念。首先,正如你能將字符串或數字做爲參數傳給函數同樣,你也能夠把函數的引用當作參數傳給函數,咱們將這種操做方式中的「函數參數」稱爲 callback (回調函數),而接收「函數參數」的函數稱之爲 高階函數

爲了體現語義化的重要性,咱們將上面的代碼從新命名來表示這個概念:

function add (x,y) {
  return x + y
}
function higherOrderFunction (x, callback) {
  return callback(x, 5)
}
higherOrderFunction(10, add)
複製代碼

這種模式是否是很熟悉?它隨處可見呀。只要你用過 JavaScript 中 的數組方法、 loadsh 或者 jQuery ,那就說明你已經使用過 callback 了。

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

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

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

通常來講,callbacks 具備兩種典型用法。第一種就是咱們上面.map_.filter的例子,這是一種將一個值計算爲另外一個值的較爲優雅的抽象化方法。咱們只需告訴它「嘿,我給你一個數組和一個函數,你用我提供給你的這個函數幫我返回一個新的值吧」。第二種用法就是上面給出的 jQuery 示例,即延遲執行一個函數直到某一特定時機。大概意思是說「嘿,給你個函數,只要 id 爲 btn的元素被點擊了你就幫我執行它」。

如今,咱們僅僅看到了同步執行的示例。但正如咱們在文章開頭時說到的:咱們開發的大多數應用中都不具有其須要的全部數據。當用戶與咱們的應用進行交互時,應用須要獲取外部數據。由此咱們已經看到了 callback 的價值所在,延遲執行一個函數直到某一特定時機

咱們無需花費太多想象力就能夠明白實踐中是如何貫徹上面那句話來進行數據獲取的。甚至是用來延遲執行一個函數,直到咱們拿到了所需的數據。來看一個咱們以前常常用到的例子,jQuery 的getJSON方法:

// 假設函數 updateUI 和 showError 已經定義過,功能如其函數名所示
const id = 'tylermcginnis'
$.getJSON({
  url: `https://api.github.com/users/${id}`,
  success: updateUI,
  error: showError,
})
複製代碼

在咱們獲取到該用戶的數據以前,咱們並不能更新應用的 UI。那麼咱們是怎麼作的呢?咱們對它說「嘿,給你個對象(非女友,別想太多),若是此次請求成功了就去調用success函數,同時把請求來的用戶數據傳給它;要是失敗了,直接調用error並把錯誤信息傳給它就好了。你不用關心每個函數的做用具體是啥,確保在你該調用他們的時候就去調用便可」。這就是利用回調函數來進行異步請求的一個很好的示例。

這一部分咱們已經知道了 callbacks 是什麼以及在同步/異步代碼中使用他們帶來的好處。但咱們還不知道使用回調函數的缺點是啥。來看一看下面的代碼,你知道發生了什麼嗎?

// 假設函數 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
  })
})
複製代碼

若是對你有幫助的話,能夠在 CodeSandbox 中看到完整的可運行代碼。

你可能已經發現,這裏已經添加了不少層回調函數,首先咱們告訴程序在 id 爲 btn的按鈕被點擊以前不要發起 AJAX 請求,等到按鈕被點擊後,咱們才發起第一個請求。若該請求成功,咱們就調用updateUI方法並傳入前兩個請求中獲取的數據。不管你第一眼看時是否理解了上面的代碼,客觀的說,這樣的代碼比以前的難讀多了。因而引出了「回調地獄」這一話題。

做爲人類,咱們的天性就是按順序思考。當代碼中的回調函數一層又一層嵌套時,就迫使你要跳出這種天然的思考方式。當代碼的閱讀方式與你天然的思考方式斷開鏈接以後,bug 就產生了。

就像大多數軟件問題的解決方案同樣,一個常規化的解決方法就是將你的回調地獄進行模塊化。

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)
})
複製代碼

CodeSandbox 中有完整代碼。

OK,函數名幫助咱們理解了到底發生了什麼。可是講真的,問題真的解決了嗎?並無。咱們僅僅是解決了回調地獄中可讀性的問題。即便是寫成了單獨的函數,咱們順序思考的天性依然被層層嵌套打斷了。

回調函數的下一個問題就要提到「控制反轉」了。你寫下一個回調函數,假設你將回調函數傳給的那個程序是可靠的,能在它該調用的時候進行調用。這實際上就是將程序的控制權轉交給另外一個程序。

當你使用相似 jQuery 或 loadsh 等第三方庫,甚至是原生 JS 時,回調函數會在正確的時間使用正確的參數被調用的假設是合理的。然而,回調函數是你與大多數第三方庫交互的接口,無論是有意仍是無心,第三方庫都有中斷與回調函數交互的可能。

function criticalFunction () {
  // It's critical that this function
  // gets called and with the correct
  // arguments.
}
thirdPartyLib(criticalFunction)
複製代碼

因爲你並非惟一調用criticalFunction的那一個,你對參數的調用徹底沒有控制權。大多數時候這都不是個問題,若是是的話,那問題可就大了。

Promise

你有沒有過不曾預定的狀況下去一個很是火爆的餐廳吃飯?遇到這種狀況時,餐廳會在有空位的時候經過某種方式聯繫你。一種古老的方式就是服務人員會記下你的名字,在有空位的狀況下喊你。隨着時代的進步,他們也開發出了新花樣,記下你的電話號碼以備有空位時短信通知你,這就容許你不用在餐廳門口死守了,還有最重要的一點就是他們能夠在任什麼時候候往你的手機推送廣告。

聽起來熟悉不?這就是 callbacks 的一種比喻啊。就像把一個回調函數傳給第三方服務同樣,咱們把本身的號碼傳給了餐廳。你指望的是餐廳有空位時聯繫你,就像你指望第三方服務在某個時刻以某種方式調用你的回調函數同樣。一旦你的號碼或者說回調函數落在他們手中,你就對其失去了控制。

幸運的是,如今有了另外一種解決方案。這種設計方案容許你保留全部控制權。你可能以前已經體驗過了,餐廳可能會給你一個蜂鳴器,相似這種:

你沒用過也不要緊,它的設計思路很簡單。這回餐館不須要記下你的名字或者電話了,取而代之給你一個這樣的裝置。在該裝置開始蜂鳴或者閃光時,就說明餐廳有空位了。在你等位的時候依然能夠作任何你想作的事,而控制權還留在你手中。並且狀況剛好跟以前相反,餐廳反而要給你掌握控制權的東西。從而沒必要控制反轉了。

這個蜂鳴器會一直處於三個不一樣狀態之一下 —— pendingfulfilledrejected

pending爲初始的默認狀態,蜂鳴器交到你手中時就是該狀態。

fulfilled就是蜂鳴器開始閃光,通知你已有空位時的狀態。

rejected是蜂鳴器通知你可能發生了什麼不順利的事,好比餐廳就要中止營業了或者是他們把你給忘了。

再次聲明,你要知道你對這個蜂鳴接收器擁有徹底的控制權。蜂鳴器變爲fulfilled狀態時,去不去吃飯是由你來決定的。若是變成rejected狀態,雖然體驗不好可是你還能夠選擇其餘餐廳吃飯,若是一直處於pending狀態的話,雖然你沒有吃上飯但你沒錯過其餘事情。

既然你成爲了蜂鳴器的主人,那咱們就來觸類旁通一下吧。

若是說把把你的號碼給了餐廳像是傳給他們一個回調函數,那麼接收這個蜂鳴器就至關於接受到了所謂的「Promise(承諾)」

按照慣例,咱們依然先問問這是爲啥?爲何會出現 Promise 呢?它的出現就是爲了使複雜的異步請求變的可控。就像前面提到的蜂鳴器,一個Promise擁有三種狀態,pendingfulfilledrejected。和蜂鳴器不一樣之處在於,這裏的三種狀態表明的是異步請求的狀態。

若是異步請求一直在執行,Promise就會保持在pending狀態下;異步請求執行成功,Promise狀態會變爲fulfilled;異步請求執行失敗,Promise狀態會變爲rejected

你已經知道了爲何會出現 Promises 及其可能出現的三種狀態,如今還有三個問題須要咱們去解答:

  1. 如何建立一個 Promise?
  2. 如何改變一個 promise 的狀態?
  3. 如何監聽 promise 在什麼時候改變了狀態?

1)如何建立一個 Promise

很直接,new出一個Promise實例便可。

const promise = new Promise();
複製代碼

2)如何改變一個 promise 的狀態?

Promise構造函數接收一個(回調)函數做爲參數,該函數接收兩個參數,resolvereject

resolve - 容許你將 promise 的狀態改成fulfilled的函數;

reject - 容許你將 promise 的狀態改成rejected的函數。

下面的代碼示例中,咱們使用setTimeout延時 2s 後調用resolve。便可將 promise 狀態變成fulfilled.

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve() // 改變狀態爲 'fulfilled'
  }, 2000)
})
複製代碼

具體改變過程看下面的過程:

能夠看到 promise 狀態從 <pending> 變爲 <resolved>

3)如何監聽 promise 在什麼時候改變了狀態?

我認爲這纔是最關鍵的問題。知道如何建立 promise 或者是改變它的狀態固然有用,但要是不知道在 promise 狀態發生改變後如何去執行一些操做的話,那仍是沒啥用。

實際上到如今咱們尚未提到 promise 究竟是個什麼東西。你new Promise的時候,僅僅是建立了一個普通的 JavaScript 對象。該對象能夠調用thencatch兩個方法,關鍵就在這裏。當 promise 的狀態變爲fulfilled,傳入到.then方法中的函數就會被調用。要是 promise 的狀態變爲rejected,那麼傳入到.catch方法中的函數就會被調用。這意思就是一旦你建立了一個 promise,一旦異步請求成功就執行你傳入到.then中的函數,失敗就執行傳入到.catch中的函數。

看一個例子,這裏再次使用setTimeout來延遲 2s 改變 promise 的狀態爲 fullfilled

function onSuccess () {
  console.log('Success!')
}
function onError () {
  console.log('💩')
}
const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve()
  }, 2000)
})
promise.then(onSuccess)
promise.catch(onError)
複製代碼

運行上面的代碼你會發現,2s 後控制檯會輸出Success!,再次梳理這個過程:首先咱們建立了一個 promise,並在 2s 後調用了 resolve函數,這一操做將 promise 的狀態改變爲 fulfilled。而後,咱們把onSuccess函數傳給了 promise 的 .then方法,經過這步操做咱們告訴 promise 在 2s 後狀態變爲fulfilled時執行onSuccess函數。

如今咱們僞裝程序發生了點兒意外,promise 的狀態變爲 rejected,從而咱們能夠調用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)
複製代碼

本身執行看看發生了什麼吧。

到這兒你已經瞭解了 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)
})
複製代碼

要是咱們能把上面回調嵌套的 AJAX 請求用 promise 包裹起來會怎樣?這樣就能夠根據請求的狀態來進行resolvereject了。讓咱們從getUser函數開始改造吧。

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

奈斯。能夠看到getUser的參數如今只需接收 id 便可了,再也不須要另外兩個回調函數了,由於不須要「控制反轉」了。這裏使用 Primise 的resolvereject函數進行替代。請求成功則執行resolve,失敗就執行reject

接下來咱們重構getWeather:

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

恩,看起來還不錯。接下來就要更新咱們的句柄,下面是咱們想要執行的工做流:

  1. 從 Github API 獲取用戶的信息;
  2. 從雅虎天氣 API 獲取由上一步所得用戶信息中的地理位置信息獲取其天氣信息;
  3. 使用用戶信息和天氣信息更新 UI。

1)從 Github API 獲取用戶的信息

$("#btn").on("click", () => {
  const userPromise = getUser('tylermcginnis')
  
userPromise.then((user) => {
})
userPromise.catch(showError)
})
複製代碼

getUser再也不接收兩個回調函數了,取而代之的是返回給咱們一個能夠調用.then.catch方法的 promise,這兩個方法在拿到用戶信息後會被調用,若是被調用的是.catch,那就說明出錯了。

2)從雅虎天氣 API 獲取由上一步所得用戶信息中的地理位置信息獲取其天氣信息

$("#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對象。

3)使用用戶信息和天氣信息更新 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)
})
複製代碼

完整代碼能夠在這裏看到

咱們的新代碼看起來還不錯,可是仍有須要改進的地方。在咱們動手改進代碼前,你須要注意 promises 的兩個特性,鏈式調用以及resolvethen的傳參。

鏈式調用

.then.catch都會返回一個新的 promise。這看起來像是一個小細節但其實很重要,由於這意味着 promises 能夠進行鏈式調用。

在下面的例子中,咱們調用getPromise會返回一個至少 2s 後resolve的 promise。從這裏開始,.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!
複製代碼

但是爲啥鏈式調用很重要?還記得上面講 callbacks 的部分提到的缺點嗎,回調函數強迫咱們進行反天然順序的思考,而 promises 的鏈式調用解決了這個問題。getPromise 運行,而後執行 logA,而後執行logB,而後...

這樣的例子還有不少,再舉一個常見的fetch API 爲例。fetch會返回給你一個resolve了 HTTP 響應的 promise。爲了獲取到實際的 JSON 數據,你須要調用.json方法。有了鏈式調用的存在,咱們就能夠順序思考問題了。

fetch('/api/user.json')
  .then((response) => response.json())
  .then((user) => {
    // user is now ready to go.
  })
複製代碼

有了鏈式調用後,咱們來繼續重構上面舉過的一個例子:

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 的參數要包含 userweather 兩個數據。咱們到這裏只有 weather,如何構造出所需的數據呢。咱們須要找出一種方法來實現它。

那麼關鍵點來了。resolve只是個函數,你傳給它的任何參數也會被傳給.then。意思就是說,在getWeather內部,若是咱們手動調用了resolve,咱們能夠傳給它userweather。而後調用鏈中第二個.then方法就會同時接收到那兩個參數。

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) => {
      // 如今,data 就是一個對象,weather 和 user是該對象的屬性
     updateUI(data)
    })
    .catch(showError)
})
複製代碼

完整代碼在這裏

如今對比一下 callbacks,來看看 promises 的強大之處吧:

// 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);
複製代碼

Async/Await

如今,promise 大幅增長了咱們異步代碼的可讀性,可是咱們可不可讓這種優點發揮的更好?假設你在 TC39 委員會工做,你有權給 JS 添加新特性,你會採起什麼方式來繼續優化下面的代碼:

$("#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,
  })
})
複製代碼

看看,這樣得有多舒服。異步代碼看起來徹底就像是同步的。咱們的大腦也很是熟悉這樣的思惟方式,無需額外成本。顯然這樣是行不通的,若是咱們運行上面的代碼,userweather僅僅是getUsergetWeather返回的 promises。但咱們但是在 TC39 呀。咱們須要告訴 JavaScript 引擎如何分辨異步函數調用與常規的同步函數。那咱們就在代碼中新增一些關鍵字來讓 JavaScript 引擎更容易識別吧。

首先,咱們能夠新增一個關鍵字到主函數上,這樣能夠告訴 JavaScript 引擎咱們要在函數內部寫異步函數調用的代碼了。就用async來作這件事吧:

$("#btn").on("click", async () => {
  const user = getUser('tylermcginnis')
  const weather = getWeather(user)
  updateUI({
     user,
     weather,
  })
})
複製代碼

這可🐂🍺了,看起來很是合理。接下來就要新增另外一個關鍵字來保證引擎確切的知道即將要調用的函數是不是異步的,而且要返回一個 promise。那麼咱們就用 await 來表示吧。這樣就告訴引擎說:「這個函數是異步的,且會返回一個 promise。不要再像你平時那樣作了,繼續執行下面的代碼的同時等待 promise 的最終結果,最後返回給我這個結果」。加入asyncawait兩個關鍵字後,咱們的代碼看起來就是這樣的了:

$("#btn").on("click", async () => {
  const user = await getUser('tylermcginnis')
  const weather = await getWeather(user.location)
  updateUI({
    user,
    weather,
  })
})
複製代碼

很贊吧。TC39 已經實現了這樣的特性,即Async/Await

async 函數返回一個 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,程序會報錯。

$("#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關鍵字時它作了兩件事:1)使函數自己返回一個 promise;2)從而使你能夠在函數內部使用await

錯誤處理

前面的代碼爲了講解方便省去了.catch對錯誤進行捕獲。在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)
  }
})
複製代碼

結語

文章太長了,翻譯到吐血。錯誤之處多多包涵。另外,你能看到這裏真的是太🐂了,給你點個贊👍。

相關文章
相關標籤/搜索