【譯】非同步JavaScript的演變史:從回撥到Promises再到Async/Await

LINJIAJUN發表於2018-12-20

我最喜歡的網站之一是BerkshireHathaway.com--它簡單,有效,並且自1997年推出以來一直正常執行。更值得注意的是,在過去的20年中,這個網站很有可能從未出現過錯誤。為什麼?因為它都是靜態的。它自20多年前推出以來幾乎一樣。如果你預先擁有所有資料,那麼構建網站非常簡單。不幸的是,現在大多數網站都沒有。為了彌補這一點,我們發明了“模式”來處理為我們的應用程式提取外部資料。像大多數事情一樣,這些模式都隨著時間的推移而發生變化。在這篇文章中,我們將分析這三種最常見的模式的優缺點,模式分別是回撥(Callbacks),Promises,和Async/Await 並從歷史背景談論它們的意義和進展。

讓我們從這些資料獲取的最初的模式開始,回撥(Callbacks)

回撥(Callbacks)

我假設你完全不知道什麼是回撥。如果我假設錯了,只需向下滾動一下跳過。

當我第一次學習程式設計時,它幫助我將函式理解為機器。這些機器可以做任何你想要的東西。他們甚至可以接受輸入並返回一個值。每臺機器上都有一個按鈕,你可以在需要機器執行時按下該按鈕,即()。

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

add(2,3) // 5 - 按下按鈕,執行機器
複製程式碼

無論我按下按鈕,你按下按鈕,或者別人按下按鈕無所謂。無論何時按下按鈕,機器都將執行。

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

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

me(2,3) // 5 - Press the button, run the machine.
you(2,3) // 5 - Press the button, run the machine.
someoneElse(2,3) // 5 - Press the button, run the machine.
複製程式碼

在上面的程式碼,我們分配add函式,三個不同的變數,me,you,和someoneElse。重要的是要注意add我們建立的原始變數和每個變數都指向記憶體中的相同位置。它們在不同的名稱下完全相同。所以,當我們呼叫me時you,或者someoneElse,就好像我們正在呼叫一樣add函式。 現在如果我們把add機器送到另一臺機器怎麼辦?請記住,按下()按鈕並不重要,如果按下它,它就會執行。

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

function addFive (x, addReference) {
  return addReference(x, 5) // 15 - Press the button, run the machine.
}

addFive(10, add) // 15
複製程式碼

你的大腦可能在這一點上有點奇怪,但這裡沒有新的東西。我們不是“按下按鈕” add,而是add作為引數傳遞addFive,重新命名它addReference,然後我們“按下按鈕”或呼叫它。

這突出了JavaScript語言的一些重要概念。首先,正如你可以將字串或數字作為引數傳遞給函式一樣,你也可以將函式的引用作為引數傳遞。當執行此操作時,作為引數傳遞的函式稱為回撥函式,並且將回撥函式傳遞給的函式稱為高階函式。

因為詞彙很重要,所以這裡的程式碼與重新命名的變數相同,以匹配他們演示的概念。

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示例中看到的,是將函式的執行延遲到特定時間。“嘿,這是這個函式。每當btn點選具有id的元素時,請繼續呼叫它。“這是我們將關注的第二個用例,”延遲執行函式直到特定時間“。

現在我們只看了同步的例子。正如我們在本文開頭所討論的那樣,我們構建的大多數應用程式都沒有預先獲得所需的所有資料。相反,他們需要在使用者與應用程式互動時獲取外部資料。我們剛剛看到回撥如何成為一個很好的用例,因為它們再次允許你“延遲執行函式直到特定時間”。看看我們如何使該句子適應資料提取並不需要太多想象力。我們可以延遲函式的執行,直到我們獲得所需的資料,而不是將函式的執行延遲到特定時間。這可能是最流行的例子,jQuery的方法:getJSON。

// updateUI and showError are irrelevant.
// Pretend they do what they sound like.

const id = 'tylermcginnis'

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

在獲得使用者資料之前,我們無法更新應用的UI。那麼我們該怎麼辦?我們說,“嘿,這是一個物件。如果請求成功,請繼續呼叫success並傳遞使用者的資料。如果沒有,請繼續呼叫error並傳遞錯誤物件。你不需要擔心每種方法的作用,只要確保在你應該的時候呼叫它們。這是使用非同步請求回撥的完美演示。

在這一點上,我們已經瞭解了回撥是什麼以及它們如何在同步和非同步程式碼中都有用處的。我們還沒有談到的是回撥的黑暗面。請看下面的程式碼。你能說出發生了什麼嗎?

// updateUI, showError, and getLocationURL are irrelevant.
// Pretend they do what they sound like.

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

如果覺得有幫助,你可以在這裡玩實時版本

請注意,我們新增了一些回撥層。首先,我們說在btn點選具有id的元素之前不要執行初始的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)
})
複製程式碼

如果覺得有幫助,你可以在這裡玩實時版本

好的,函式名稱可以幫助我們更加了解正在發生的事情,但客觀上是“更好”嗎?並不是很多。我們只是在回撥地獄的可讀性問題上加了一個創可貼。問題仍然存在,我們自然地按順序思考,即使有額外的功能,巢狀的回撥也會使我們擺脫順序的思維方式。

下一期回撥與控制反轉有關。當你編寫一個回撥時,假設你給回撥的程式是負責的,並且會在它應該的時候(並且只有當它)時呼叫它。實際上是將程式控制權轉換為另一個程式。當您處理jQuery,lodash甚至vanilla JavaScript等庫時,可以安全地假設使用正確的引數在正確的時間呼叫回撥函式。但是,對於許多第三方庫,回撥函式是您與它們互動方式的介面。第三方庫無論是故意的還是偶然的,都可以打破他們與你的回撥互動的方式,這是完全合情合理的。

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

thirdPartyLib(criticalFunction)
複製程式碼

既然你不是那個呼叫者criticalFunction,你就可以控制呼叫它的時間和引數。大多數時候這不是問題,但是當它出現問題時,這是一個很大的問題。

Promises

你有沒有預訂去過一個繁忙的餐館?當這種情況發生時,餐廳需要一種方法在桌子開啟時與你聯絡。從歷史上看,當你的桌子準備就緒時,他們只會取你的名字並大喊大叫。然後,自然而然地,他們決定開始變幻想。一個解決方案是,一旦桌子開啟,他們就會取你的號碼並給你發簡訊,而不是取你的名字。這使您可以超出大喊大叫的範圍,但更重要的是,它允許他們隨時根據需要定位你的手機廣告。聽起來有點熟?這應該!好吧,也許不應該。這是回撥的隱喻!將你的號碼提供給餐館就像給第三方服務提供回撥功能一樣。你希望餐廳在桌子開啟時給您發簡訊,就像你一樣期望第三方服務在何時以及如何表達時呼叫你的功能。一旦你的號碼或回叫功能掌握在他們手中,您就失去了所有控制權。

值得慶幸的是,存在另一種解決方案。一個設計,允許您保持所有控制。你甚至可能以前都經歷過 - 這是他們給你的小嗡嗡聲。你知道,這個。

【譯】非同步JavaScript的演變史:從回撥到Promises再到Async/Await
如果你之前從未使用過,那麼這個想法很簡單。他們沒有取你的名字或號碼,而是給你這個裝置。當裝置開始嗡嗡作響併發光時,你的桌子就準備好了。當你等待桌子開啟時,你仍然可以做任何你想做的事,但現在你不必放棄任何東西。事實上,恰恰相反。他們必須給你一些東西。沒有控制倒置。

蜂鳴器始終處於三種不同狀態中的一種- pending,fulfilled或rejected。

pending是預設的初始狀態。當他們給你蜂鳴器時,它處於這種狀態。

fulfilled 當蜂鳴器閃爍並且你的桌子準備就緒時蜂鳴器所在的狀態。

rejected當出現問題時,蜂鳴器處於狀態。也許餐廳即將關閉,或者他們忘了有人在晚上出租餐廳。

同樣,要記住的重要一點是,你,蜂鳴器的接收器,擁有所有的控制權。如果蜂鳴器進入fulfilled,你可以去你的桌子。如果它被放入fulfilled並且你想忽略它,那麼很酷,你也可以這樣做。如果它被放入rejected,那很糟糕,但你可以去別的地方吃。如果沒有任何事情發生並且它留在pending,你永遠不會吃,但你實際上並沒有任何東西。

現在你已成為餐廳蜂鳴器的主人,讓我們將這些知識應用到重要的事情上。

如果給餐廳你的號碼就像給他們一個回撥功能,接收這個小小的東西就像收到所謂的“Promise”。

一如既往,讓我們從為什麼開始吧。為什麼Promises存在?它們的存在使得使非同步請求更易於管理的複雜性。完全像蜂鳴器,一個 Promise可以處於三種狀態之一pending,fulfilled或者rejected。與蜂鳴器不同,它們代表表示餐館桌子狀態的這些狀態,它們代表非同步請求的狀態。

如果非同步請求仍在進行中,則Promise狀態為pending。如果非同步請求成功完成,則Promise狀態將更改為fulfilled。如果非同步請求失敗,Promise則將更改為狀態rejected。蜂鳴器比喻很有意義,對嗎?

既然你已經理解了Promise存在的原因以及它們可以存在的不同狀態,那麼我們還需要回答三個問題。 1、如何創造一個Promise? 2、如何改變Prommise的狀態? 3、當Promise的狀態發生變化時,如何監聽?

如何創造一個Promise?

這個很直接。建立一個new例項Promise。

const promise = new Promise()
複製程式碼

如何改變Prommise的狀態?

該Promise建構函式接受一個引數,一個(回撥)函式。這個函式將傳遞兩個引數,resolve和reject。

resolve - 一個允許你更改Promise狀態的功能 fulfilled

reject- 一個允許你更改Promise狀態的功能rejected。

在下面的程式碼中,我們使用setTimeout等待2秒然後呼叫resolve。這將改變Promise的狀態fulfilled。

const promise = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve() // Change status to 'fulfilled'
  }, 2000)
})

複製程式碼

我們可以通過在建立它之後立即記錄promise來看到這種變化,然後resolve在呼叫之後大約2秒後再次記錄。

【譯】非同步JavaScript的演變史:從回撥到Promises再到Async/Await

注意Promise從pending到resolved。

當Promise的狀態發生變化時,如何監聽?

在我看來,這是最重要的問題。很酷我們知道如何建立Promise並改變其狀態,但如果我們在狀態發生變化後不知道如何做任何事情,那就毫無價值。

我們還沒有談到的一件事是Promise實際上是什麼。當你建立一個時new Promise,你真的只是建立一個普通的舊JavaScript物件。該物件可以呼叫兩個方法then,和catch。這是關鍵。當promise的狀態更改fulfilled為時,.then將呼叫傳遞給的函式。當promise的狀態更改rejected為時,.catch將呼叫傳遞給的函式。這意味著一旦你建立了一個promise,如果非同步請求成功,你將傳遞你想要執行的函式.then。如果非同步請求失敗,你將傳遞要執行的功能.catch。

我們來看一個例子吧。我們將setTimeout再次使用fulfilled在兩秒鐘(2000毫秒)之後將Promise的狀態更改為。

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

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

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

promise.then(onSuccess)
promise.catch(onError)
複製程式碼

如果執行上面的程式碼,你會注意到大約2秒後,您將在控制檯中看到“成功!”。這種情況再次發生的原因是兩件事。首先,當我們建立了promise時,我們resolve在〜2000毫秒之後呼叫- 這改變了promise的狀態fulfilled。其次,我們將onSuccess函式傳遞給promises的.then方法。通過這樣做,我們告訴Promise,onSuccess當Promise的狀態改變為fulfilled〜2000毫秒後它所做的時,呼叫。

現在讓我們假裝發生了一些不好的事情,我們想要改變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。我們將遵循相同的策略。我們將使用使用resolve和reject而不是接收onSuccess和onFailure回撥函式。

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

看起來不錯。現在我們需要更新的最後一件事是我們的點選處理程式。請記住,這是我們想要採取的流程。

從Github API獲取使用者的資訊。 1、使用使用者的位置從Yahoo Weather API獲取他們的天氣。 2、使用使用者資訊及其天氣更新UI。 3、讓我們從#1開始 - 從Github API獲取使用者的資訊。

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

  userPromise.then((user) => {

  })

  userPromise.catch(showError)
})
複製程式碼

請注意,現在它不是getUser接受兩個回撥函式,而是返回一個我們可以呼叫.then和.catch啟用的Promise。如果.then被呼叫,將使用使用者的資訊呼叫它。如果.catch被呼叫,它將被呼叫錯誤。

接下來讓我們做#2 - 使用使用者的位置來獲取他們的天氣。

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

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

    })

    weatherPromise.catch(showError)
  })

  userPromise.catch(showError)
})
複製程式碼

請注意,我們遵循我們在#1中完全相同的模式,但現在我們呼叫getWeather它傳遞給user我們的物件userPromise。

最後,#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)
})
複製程式碼

這是你可以使用的完整程式碼。 新程式碼看起來更好,但我們仍然可以做出一些改進。在我們做出這些改進之前,你需要知道Promise到兩個功能:鏈式呼叫和將引數從resolve傳遞到then。

鏈式呼叫

.then和.catch都會返回一個新的Promise。這似乎是一個小細節,但它很重要,因為它意味著Promise可以被鏈式呼叫。

在下面的示例中,我們呼叫getPromise它返回一個將在至少2000毫秒內解析的promise。從那裡,因為.then將返回一個Promise,我們可以繼續將我們的.thens連結在一起,直到我們丟擲一個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 runs then logA runs then logB runs then...。

這樣你就可以看到另一個示例,這是使用fetchAPI 時的常見用例。fetch將返回一個將通過HTTP響應解決的Promise。要獲得實際的JSON,你需要呼叫.json。由於連結,我們可以按順序思考這個問題。

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

現在我們知道有關鏈式呼叫,讓我們來重構我們早期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) => {
      // We need both the user and the weather here.
      // Right now we just have the weather
      updateUI() // ????
    })
    .catch(showError)
})
複製程式碼

它看起來好多了,但現在我們遇到了一個問題。你能發現它嗎?在第二個.then我們要呼叫updateUI。問題是,我們需要通過updateUI這兩個user和weather。目前我們如何設定,我們只收到weather,而不是user。不知何故,我們需要找出一種方法來使它成為一個Promise,即getWeather使用user和來解決回報weather。

這是關鍵。resolve只是一個功能。您傳遞給它的任何引數都將傳遞給給定的函式.then。這是什麼意思是,裡面getWeather,如果我們呼叫resolve我們自己,我們可以通過它weather和user。然後,.then我們鏈中的第二個方法將同時接收user和weather作為引數。

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 then getWeather then update the UI with the data。 現在很明顯,promises會大大提高非同步程式碼的可讀性,但有沒有辦法讓它變得更好?假設你是TC39委員會成員,並且你有能力為JavaScript語言新增新功能。你將採取哪些步驟來改進此程式碼?

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

正如我們所討論的那樣,程式碼讀得非常好。正如我們的大腦工作一樣,它是按順序排列的。我們遇到的一個問題是我們需要將data(users)從第一個非同步請求一直到最後一個.then。這不是什麼大不了的事,但它讓我們改變了我們的getWeather功能,也傳遞了它users。如果我們編寫非同步程式碼的方式與編寫同步程式碼的方式相同怎麼辦?如果我們這樣做了,那麼這個問題就會徹底消失,而且它仍會按順序讀取。這是一個想法。

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

  updateUI({
    user,
    weather,
  })
})
複製程式碼

好吧,那會很好。我們的非同步程式碼看起來就像我們的同步程式碼。我們的大腦沒有額外的步驟需要採取,因為我們已經非常熟悉這種思維方式。可悲的是,這顯然是行不通的。如你所知,如果我們要執行上面的程式碼,user並且weather兩者都只是Promise,因為那是什麼getUser並getWeather返回。但請記住,我們正在使用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。

不相信我?這是我們的實時程式碼,現在我們已經新增了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,則會出現錯誤。

$("#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並使它可以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)
  }
})
複製程式碼

(完)

連結

後記

以上譯文僅用於學習交流,水平有限,難免有錯誤之處,敬請指正。

相關文章