异步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)
  }
})

转载
原文连接

相关文章
相关标签/搜索