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

zhaofeihao發表於2019-03-29

原文連結 The Evolution of Async JavaScript: From Callbacks, to Promises, to Async/Await

注:本文為我的課程《高階 JavaScript》中的一部分,如果你喜歡本文,歡迎你來看看我的課程。

BerkshireHathaway.com 是我最喜歡的網站之一,因為它簡單、高效,而且自從 1997 年建立以來一直執行良好。更??的是在過去的二十年裡這個網站很有可能從未出現過 bug。

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

也就是說如果你預先擁有所有資料,那麼建站就會變得非常簡單。不幸的是,現如今的大多數站點都不是這樣的。為了彌補這方面的缺點,我們為我們的系統發明了各種「模式」來應對這種需要獲取外部資料的情況。

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

Callbacks

這裡會假設你完全不瞭解何為 callbacks。要是我假設有誤,稍微向下滑一下即可。

在我剛開始學習程式設計的時候,我把函式看成一臺機器。這些機器可以做任何你想讓他們完成的工作。甚至是接收一個輸入然後返回一個值。每臺機器都有一個按鈕,你可以在你想讓他們運轉的時候按下這個按鈕,在函式裡,也就是()

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

這個按鈕是何時被誰按下的並不重要,機器只管去執行。

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 的一種比喻啊。就像把一個回撥函式傳給第三方服務一樣,我們把自己的號碼傳給了餐廳。你期望的是餐廳有空位時聯絡你,就像你期望第三方服務在某個時刻以某種方式呼叫你的回撥函式一樣。一旦你的號碼或者說回撥函式落在他們手中,你就對其失去了控制。

幸運的是,現在有了另一種解決方案。這種設計方案允許你保留所有控制權。你可能之前已經體驗過了,餐廳可能會給你一個蜂鳴器,類似這種:

[譯] JavaScript 非同步演進史,從 Callbacks, Promises 到 Async/Await
你沒用過也沒關係,它的設計思路很簡單。這回餐館不需要記下你的名字或者電話了,取而代之給你一個這樣的裝置。在該裝置開始蜂鳴或者閃光時,就說明餐廳有空位了。在你等位的時候依然可以做任何你想做的事,而控制權還留在你手中。而且情況恰好跟之前相反,餐廳反而要給你掌握控制權的東西。從而不必控制反轉了。

這個蜂鳴器會一直處於三個不同狀態之一下 —— 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)
})
複製程式碼

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

[譯] JavaScript 非同步演進史,從 Callbacks, Promises 到 Async/Await
可以看到 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)
  }
})
複製程式碼

結語

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

相關文章