异步 JavaScript 的演变:从回调到 Promises,再到 Async/Await

2025-06-07

异步 JavaScript 的演变:从回调到 Promises,再到 Async/Await

在本文中,您将了解三种最流行的 JavaScript 异步模式(回调、Promises 和 Async/Await)的历史背景以及其优缺点。


这最初发表在TylerMcGinnis.com上,是其高级 JavaScript课程的一部分


视频

邮政

BerkshireHathaway.com是我最喜欢的网站之一——它简洁高效,自 1997 年上线以来一直运行良好。更令人称奇的是,在过去的 20 年里,这个网站几乎从未出现过任何 bug。为什么?因为它完全是静态的。自 20 多年前上线以来,它几乎一直保持原样。事实证明,如果您预先准备好所有数据,那么构建网站其实非常简单。可惜的是,如今大多数网站都没有这样做。为了弥补这一点,我们发明了一些“模式”来处理应用程序获取外部数据的过程。与大多数事物一样,这些模式各有优缺点,并且会随着时间的推移而发生变化。在本文中,我们将分析三种最常见的模式、、和的优缺点CallbacksPromisesAsync/Await从历史背景探讨它们的意义和发展历程。

让我们从这些数据获取模式的 OG——回调开始。

回调

我假设你对回调一无所知。如果我的假设错误,请向下滚动一点。

在我刚开始学习编程的时候,我把函数想象成机器,这很有帮助。这些机器可以做任何你想让它们做的事情。它们甚至可以接受输入并返回值。每台机器上都有一个按钮,当你想让机器运行时,你可以按下它()。

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

add(2,3) // 5 - Press the button, run the machine.
Enter fullscreen mode Exit fullscreen mode

无论按下按钮,按下按钮,还是别人按下按钮,都没关系。只要按钮被按下,不管你愿意与否,机器就会运转。

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.
Enter fullscreen mode Exit fullscreen mode

在上面的代码中,我们将add函数赋值给三个不同的变量:meyousomeoneElse。需要注意的是,原始变量add和我们创建的每个变量都指向内存中的同一位置。它们实际上是同一个东西,只是名字不同。所以当我们调用meyou或 时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
Enter fullscreen mode Exit fullscreen mode

你的大脑可能对此有点奇怪,不过这没什么新鲜的。add我们不是在 上“按下按钮”,而是将add参数传递给addFive,将其重命名为addReference,然后我们“按下按钮”或调用它。

这突出了 JavaScript 语言的一些重要概念。首先,就像你可以将字符串或数字作为参数传递给函数一样,你也可以将函数的引用作为参数传递。当你这样做时,你作为参数传递的函数称为回调函数,而你将回调函数传递给的函数称为高阶函数

因为词汇很重要,所以这里有相同的代码,但变量已重新命名,以匹配它们所演示的概念。

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

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

higherOrderFunction(10, add)
Enter fullscreen mode Exit fullscreen mode

这种模式看起来很熟悉,它无处不在。如果你曾经使用过 JavaScript 数组的任何方法,那么你一定用过回调。如果你曾经使用过 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')
)
Enter fullscreen mode Exit fullscreen mode

一般来说,回调有两种常见的用例。第一种,也就是我们在.map_.filter示例中看到的,是对将一个值转换为另一个值的良好抽象。我们会说:“嘿,这是一个数组和一个函数。请你根据我提供的函数获取一个新的值。” 第二种,也就是我们在 jQuery 示例中看到的,是将函数的执行延迟到特定时间。“嘿,这是这个函数。请你每当 id 为 的元素btn被点击时调用它。” 我们将重点关注第二种用例,“将函数的执行延迟到特定时间”。

目前我们只讨论了同步的示例。正如我们在本文开头所讨论的,我们构建的大多数应用并没有预先准备好所需的所有数据。相反,它们需要在用户与应用交互时获取外部数据。我们刚刚看到了回调是如何成为这方面一个很好的用例的,因为它们允许你“将函数的执行延迟到特定时间”。不难想象,我们可以如何将这句话运用到数据获取中。与其将函数的执行延迟到特定时间,不如将函数的执行延迟到获得所需数据为止。以下可能是最常见的例子,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,
})
Enter fullscreen mode Exit fullscreen mode

在获得用户数据之前,我们无法更新应用的 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
  })
})
Enter fullscreen mode Exit fullscreen mode

如果有帮助的话,您可以在这里试用实时版本

注意,我们添加了更多层级的回调。首先,我们声明在 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)
})
Enter fullscreen mode Exit fullscreen mode

如果有帮助的话,您可以在这里试用实时版本

好吧,函数名确实能帮助我们理解代码,但客观上真的“更好”吗?并没有。我们只是给回调地狱的可读性问题打了个包。问题仍然存在,因为我们本能地会按顺序思考,即使有了额外的函数,嵌套回调也会打破我们的顺序思维。


回调的下一个议题与控制反转有关。当你编写回调时,你假设你将回调传递给的程序负责并将在(且仅在)应该调用它时调用它。你实际上是将程序的控制权反转给了另一个程序。当你处理像 jQuery、lodash 或甚至 vanilla JavaScript 这样的库时,可以安全地假设回调函数将在正确的时间使用正确的参数调用。但是,对于许多第三方库来说,回调函数是你与它们交互的接口。第三方库完全有可能(无论是有意还是无意)破坏它们与回调的交互方式。

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

thirdPartyLib(criticalFunction)
Enter fullscreen mode Exit fullscreen mode

由于调用者并非你criticalFunction,你对何时调用以及使用什么参数完全没有控制权。大多数情况下,这不算什么问题,但一旦成了问题,那就麻烦大了。


承诺

你有没有遇到过没有预订就去人满为患的餐厅的经历?这种情况发生时,餐厅需要一种方式,以便在有空位时及时联系你。以前,他们只会记下你的名字,然后在你的座位准备好后大声喊出来。后来,他们自然而然地想出了一个更奇特的办法。一个解决方案是,不记下你的名字,而是记下你的电话号码,然后在有空位时给你发短信。这样,你就不会被人大声喊叫,但更重要的是,他们可以随时向你的手机投放广告。听起来很熟悉?应该很熟悉!好吧,也许不熟悉。这是一个回调函数的隐喻!把你的电话号码给餐厅,就像把回调函数给第三方服务一样。你期望餐厅在有空位时给你发短信,就像你期望第三方服务在他们承诺的时间和方式调用你的回调函数一样。然而,一旦你的电话号码或回调函数落入他们手中,你就完全失去了控制。

谢天谢地,还有另一种解决方案。它的设计理念就是让你保留所有的控制权。你可能以前就体验过——就是他们给你的那个小蜂鸣器。就是这个。

餐厅蜂鸣器

如果你以前没用过,它的原理很简单。他们不会记录你的姓名或号码,而是给你一个设备。当设备开始嗡嗡作响并发光时,你的桌位就准备好了。在等待桌位空出来的时候,你仍然可以做任何你想做的事情,但现在你不用放弃任何东西了。事实上,情况恰恰相反。他们必须给一些东西。这里没有控制反转。

蜂鸣器将始终处于三种不同状态之一 - pendingfulfilledrejected

pending是默认的初始状态。当他们给你蜂鸣器时,它就处于这个状态。

fulfilled是蜂鸣器闪烁时的状态,表示您的桌子已准备好。

rejected是发生故障时蜂鸣器的状态。也许餐厅即将打烊,或者他们忘记有人把餐厅租出去过夜了。

再次强调,重要的是要记住,你,作为蜂鸣器的接收者,拥有所有的控制权。如果蜂鸣器被触发fulfilled,你可以回到你的餐桌。如果蜂鸣器被触发,fulfilled而你想忽略它,很酷,你也可以这么做。如果蜂鸣器被触发rejected,那就很糟糕了,但你可以去其他地方吃饭。如果什么也没发生,蜂鸣器一直处于触发状态pending,你就永远吃不到饭了,但你实际上什么也没损失。

既然您已经熟悉了餐厅蜂鸣器,让我们将这些知识应用到重要的事情上。

如果向餐厅提供您的电话号码就像给他们一个回调函数,那么接收这个小嗡嗡声就像接收所谓的“承诺”。

和往常一样,我们先来谈谈为什么。Promise 存在的意义是什么?它们的存在是为了降低异步请求的复杂性。与蜂鸣器类似,aPromise可以处于三种状态之一:pendingfulfilledrejected。与蜂鸣器不同的是,这些状态代表的不是餐厅餐桌的状态,而是异步请求的状态。

如果异步请求仍在进行中,Promise的状态将为pending。如果异步请求成功完成, 的Promise状态将更改为fulfilled。如果异步请求失败, 的Promise状态将更改为rejected。蜂鸣器的比喻很贴切,对吧?

现在您已经了解了 Promises 存在的原因以及它们可以处于的不同状态,我们还需要回答三个问题。

1) 如何创建 Promise?
2) 如何更改 Promise 的状态?
3) 如何监听 Promise 状态的变化?

1)如何创建Promise?

这个很简单。你创建一个new的实例Promise

const promise = new Promise()
Enter fullscreen mode Exit fullscreen mode
2)如何改变承诺的状态?

构造Promise函数接受一个参数,即一个回调函数。该函数将传递两个参数,resolvereject

resolve- 允许你将承诺的状态更改为fulfilled

reject- 一个允许您将承诺的状态更改为的功能rejected

在下面的代码中,我们使用setTimeout等待 2 秒,然后调用resolve。这会将 Promise 的状态更改为fulfilled

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

我们可以在创建承诺后立即记录该承诺,然后在大约 2 秒后再次记录该承诺,从而看到这一变化resolve

使用 resolve 将承诺的状态更改为 fulfilled

请注意承诺从<pending><resolved>

3)如何监听承诺状态的变化?

在我看来,这是最重要的问题。我们知道如何创建 Promise 并改变它的状态,这很酷,但如果我们不知道状态改变后该做什么,那就毫无意义了。

我们还没有讨论的一件事是承诺实际上是什么。当您创建时new Promise,您实际上只是在创建一个普通的 JavaScript 对象。此对象可以调用两个方法,thencatch。这是关键。当承诺的状态变为时fulfilled,传递给的函数.then将被调用。当承诺的状态变为时rejected,传递给的函数.catch将被调用。这意味着一旦您创建了一个承诺,您将在异步请求成功时传递您想要运行的函数给.then。您将在异步请求失败时传递您想要运行的函数给.catch

我们来看一个例子。我们将使用setTimeoutagain 将 Promise 的状态更改为fulfilled两秒后(2000 毫秒)。

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

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

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

promise.then(onSuccess)
promise.catch(onError)
Enter fullscreen mode Exit fullscreen mode

如果你运行上面的代码,你会注意到大约 2 秒后,你会在控制台中看到“Success!”。再次强调,发生这种情况的原因有两个。首先,当我们创建 Promise 时,我们resolve在大约 2000 毫秒后调用了 —— 这会将 Promise 的状态更改为fulfilled。其次,我们将onSuccess函数传递给了 Promise 的.then方法。通过这样做,我们告诉 PromiseonSuccess在 Promise 的状态更改为 时调用fulfilled该方法,而它在大约 2000 毫秒后确实发生了更改。

现在假设发生了一些不好的事情,我们想将 Promise 的状态改为。我们不会rejected调用 ,而是调用resolvereject

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

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

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

promise.then(onSuccess)
promise.catch(onError)
Enter fullscreen mode Exit fullscreen mode

现在,由于我们调用了,所以这次将调用函数onSuccess,而不是函数onErrorreject


现在您已经了解了 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)
})
Enter fullscreen mode Exit fullscreen mode

有没有办法在这里使用 Promise API 而不是回调?如果我们把 AJAX 请求包装在 Promise 中会怎么样?这样我们就可以根据请求的进展情况简单地执行resolve或。让我们从 开始吧rejectgetUser

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

不错。注意到 的参数getUser变了。它不再接收idonSuccess和 ,onFailure而是接收id。由于我们不再反转控制,所以不再需要另外两个回调函数。取而代之的是,我们使用 Promise 的resolvereject函数。resolve如果请求成功, 将被调用;reject如果发生错误, 将被调用。

接下来让我们重构getWeather。我们将在这里遵循相同的策略。我们不再接受onSuccessonFailure回调函数,而是使用resolvereject

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

看起来不错。现在我们需要更新的最后一件事是点击处理程序。记住,这是我们想要采用的流程。

1) 从 Github API 获取用户信息。2
) 使用用户位置从 Yahoo Weather API 获取其天气信息。3
) 使用用户信息及其天气信息更新 UI。

让我们从#1开始——从 Github API 获取用户信息。

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

  userPromise.then((user) => {

  })

  userPromise.catch(showError)
})
Enter fullscreen mode Exit fullscreen mode

注意,现在它不再getUser接受两个回调函数,而是返回一个可以调用的 Promise .then.catch如果.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)
})
Enter fullscreen mode Exit fullscreen mode

请注意,我们遵循与#1中完全相同的模式,但现在我们调用并从中获得的对象getWeather进行传递useruserPromise

最后,#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)
})
Enter fullscreen mode Exit fullscreen mode

这是您可以尝试的完整代码。

我们的新代码已经更好了,但仍有一些地方可以改进。不过,在进行这些改进之前,你还需要了解 Promise 的两个特性:链式调用和从resolvePromise传递参数then

链接

.then都会.catch返回一个新的 Promise。这看似一个小细节,但却很重要,因为它意味着 Promise 可以串联起来。

在下面的例子中,我们调用getPromise,它会返回一个至少在 2000 毫秒内解析的 Promise。从这里开始,因为.then会返回一个 Promise,所以我们可以继续将.thens 链接在一起,直到抛出一个new Error被该方法捕获的 s .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!
Enter fullscreen mode Exit fullscreen mode

很酷,但这为什么如此重要?还记得在回调部分我们讨论过回调的缺点之一,那就是它会迫使你脱离自然的、顺序的思维方式。当你将多个 Promise 链接在一起时,它不会迫使你脱离这种自然的思维方式,因为链接的 Promise 是顺序的getPromise runs then logA runs then logB runs then...

为了让您再看一个例子,这是使用fetchAPI 时的一个常见用例。fetch它将返回一个 Promise,该 Promise 将通过 HTTP 响应进行解析。要获取实际的 JSON,您需要调用.json。由于链式调用,我们可以按顺序来思考这个问题。

fetch('/api/user.json')
  .then((response) => response.json())
  .then((user) => {
    // user is now ready to go.
  })
Enter fullscreen mode Exit fullscreen mode

现在我们了解了链接,让我们重构之前的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)
})
Enter fullscreen mode Exit fullscreen mode

看起来好多,但现在我们遇到了一个问题。你能发现吗?在第二个函数中,.then我们想要调用updateUI。问题是我们需要updateUI同时传递userweather。目前的设置是,我们只接收weather,而不是user。我们需要想办法让getWeather返回的 Promise 同时通过user和得到解决weather

关键在于。resolve只是一个函数。传递给它的任何参数都会传递给 的函数.then。这意味着,在 的内部getWeather,如果我们调用自身,我们可以将resolve传递给它。然后,我们链中的第二个方法将同时接收作为参数。weatheruser.thenuserweather

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)
})
Enter fullscreen mode Exit fullscreen mode

你可以在这里试用最终代码

在我们的点击处理程序中,您可以真正看到承诺相对于回调的强大威力。

// 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);
Enter fullscreen mode Exit fullscreen mode

遵循这种逻辑感觉很自然,因为这是我们习惯的思维方式,按顺序进行getUser then getWeather then update the UI with the data


现在很明显,Promise 极大地提高了异步代码的可读性,但还有什么方法可以让它变得更好呢?假设你是 TC39 委员会的成员,并且拥有为 JavaScript 语言添加新功能的全部权力。你会采取哪些步骤(如果有的话)来改进这段代码?

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

正如我们之前讨论过的,代码读起来非常流畅。就像我们的大脑一样,它是按顺序运行的。我们遇到的一个问题是,我们需要将数据 ( users) 从第一个异步请求一直传递到最后一个.then。这没什么大不了的,但它迫使我们修改getWeather函数,以便将 也传递users。如果我们以编写同步代码的方式编写异步代码会怎么样?如果我们这样做了,这个问题就会完全消失,代码仍然按顺序读取。这里有一个想法。

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

  updateUI({
    user,
    weather,
  })
})
Enter fullscreen mode Exit fullscreen mode

嗯,那太好了。我们的异步代码看起来和同步代码一模一样。我们的大脑不需要做任何额外的步骤,因为我们已经非常熟悉这种思维方式了。可惜的是,这显然行不通。正如你所知,如果我们运行上面的代码,userweather都只是承诺,因为这就是getUsergetWeather返回的结果。但请记住,我们是在 TC39 上。我们完全有能力为这门语言添加任何我们想要的功能。照这样下去,这段代码很难运行。我们必须以某种方式教会 JavaScript 引擎动态地识别异步函数调用和常规同步函数调用之间的区别。让我们在代码中添加一些关键字,让引擎更容易理解。

首先,我们在 main 函数中添加一个关键字。这可以让引擎知道,在这个函数内部,我们将进行一些异步函数调用。我们就用它来实现async这一点。

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

  updateUI({
    user,
    weather,
  })
})
Enter fullscreen mode Exit fullscreen mode

酷!这看起来很合理。接下来,让我们添加另一个关键字,让引擎确切地知道被调用的函数何时是异步的,并且会返回一个 Promise。我们使用await。例如,“嘿,引擎。这个函数是异步的,并且返回一个 Promise。与其像平常一样继续执行,不如先‘等待’ Promise 的最终值,然后返回它再继续执行”。有了新的asyncawait关键字,我们的新代码将如下所示。

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

  updateUI({
    user,
    weather,
  })
})
Enter fullscreen mode Exit fullscreen mode

真是太巧妙了!我们发明了一种合理的方法,让异步代码看起来和运行起来都像同步代码一样。接下来的一步是说服 TC39 上的某个人,让他们相信这是一个好主意。幸运的是,正如你可能已经猜到的那样,我们不需要做任何说服工作,因为这个特性已经是 JavaScript 的一部分了,它叫做Async/Await

不相信?下面是添加了 Async/Await 的实时代码。欢迎大家尝试一下。


异步函数返回一个承诺

现在你已经了解了 Async/Await 的好处,让我们讨论一些重要的细节。首先,每当你async向一个函数添加内容时,该函数都会隐式返回一个 Promise。

async function getPromise(){}

const promise = getPromise()
Enter fullscreen mode Exit fullscreen mode

尽管getPromise实际上是空的,但它仍然会返回一个承诺,因为它是一个async函数。

如果async函数返回一个值,该值也会被包装在一个 Promise 中。这意味着你必须使用.then来访问它。

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

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

没有 async 的 await 是不好的

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,
  })
})
Enter fullscreen mode Exit fullscreen mode

我是这样想的。当你给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)
  }
})
Enter fullscreen mode Exit fullscreen mode

这最初发表在TylerMcGinnis.com上,是其高级 JavaScript课程的一部分

文章来源:https://dev.to/tylermcginnis/the-evolution-of-async-javascript-from-callbacks-to-promises-to-asyncawait-34h1
PREV
求职面试的自信工具箱
NEXT
JavaScript 模块:从 IIFE 到 CommonJS 再到 ES6 模块