[翻译]JavaScript异步进化史:Callbacks,Promises,Async/Await 下篇

in #cn5 years ago

原文 The Evaluation of Async JavaScript :From Callbacks, to Promises, to Async/Await

本文上篇请翻阅[翻译]JavaScript异步进化史:Callbacks,Promises,Async/Await 上篇

Promise

你有没有曾经在没有预订的情况下去过一个繁忙的餐厅?当这种情况发生时,餐厅需要一种方法在有空桌时与你联系。一般而言,他们只会取得你的名字并在有空桌时大声呼喊。然后,自然而然地,他们会开始想要变通。一个解决方案是,他们就会得到你的电话号码而不是你的名字,然后在有空桌时给你发信息。这可以使你能够散步到服务员们大喊的范围以外,但更重要的是,如果他们想的话,他们可以给你的电话发广告。听起来有点熟?是的!好吧,也许也不是。这是回调的隐喻!将你的号码提供给餐厅就像给第三方库提供回调函数一样。你希望餐厅在有空桌时给你发短信,就像你希望第三方服务如他们所说的那样会在何时以及如何调用你的函数一样。一旦你的号码或回调函数落入他们手中,你就失去了所有控制权。

庆幸的是,存在另一种解决方案。一个被设计成允许你保留所有控制权的方案。你甚至可能以前都体验过了,就是他们给你的小蜂鸣器。你知道的,就是这个东西:

如果你之前从未使用过这玩意儿,那么这个概念很简单。他们没有取你的名字或号码,而是给你这个设备。当设备开始嗡嗡作响并发光时,意味着你的桌子已经准备好了。当你在等待空桌时,你仍然可以做任何你想做的事,但现在你不必失去任何东西。事实上,恰恰相反。他们反而必须给你一些东西,这里没有控制的转化。

  • 蜂鸣器将始终处于三种不同状态之一:pending(待决),fulfilled(已实现)或rejected(已驳回)。

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

  • fulfilled是蜂鸣器闪烁并且你的桌子已准备就绪时所处的状态。

rejected是当出现问题时蜂鸣器所处的状态。也许餐厅即将关闭,或者他们忘了有人在晚上包下了餐厅。

再次强调,应该记住的重点是,你这个蜂鸣器的接收人,拥有所有的控制权。如果蜂鸣器转变为fulfilled状态,你可以去你的桌子坐下开始点餐,但是如果你想忽略它,这很酷,当然你也是可以这样做的。如果它转变为rejected状态,那很糟糕,但你可以去别的地方吃饭。如果没有任何事情发生并且它一直处于pending状态,你会永远都吃不到东西,但是实际上你也并没有失去任何东西。

现在你已成为餐厅蜂鸣器的主人,让我们将这些知识应用到相关的事情上。

如果给餐厅你的号码就像给他们一个回调函数,那么接受这个小小的蜂鸣器就像收到他们所谓的“承诺”。

跟往常一样,让我们从为什么开始吧。为什么Promises存在?因为它们的存在使得复杂的异步请求更易于管理。正如蜂鸣器一样,Promise可以处于三种状态之一,pending,fulfilled或rejected。与蜂鸣器不同的是,蜂鸣器代表餐厅桌子的状态,而Promise代表异步请求的状态。

如果异步请求仍在进行中,则Promise将处于pending状态。如果异步请求成功,则Promise将更改为fulfilled的状态。如果异步请求失败,则Promise将更改为rejected状态。蜂鸣器这个比喻很有意义,对吗?

既然你已经理解了Promise存在的原因以及它们的不同状态,那么我们还需要去回答三个问题。

  • 你如何创建一个Promise?

  • 你如何改变Promise的状态?

  • 你如何监听Promise的状态变化?

1)你如何创建一个Promise?

这里有一个最直接的做法,你可以new一个Promise对象的实例:

2)你如何改变Promise的状态?

Promise构造函数使用一个回调函数作为参数,这个函数用来传递两个参数,resolve和reject。

resolve:一个允许你将promise的状态改变为fulfilled的函数

reject:一个允许你将promise的状态改变为rejected的函数

在下面的代码中,我们使用setTimeout等待两秒后触发resolve。这将会使promise的状态转变为fulfilled。

我们可以输出记录来看一下promise两秒前和两秒后执行了resolve的状态变化。

3)你如何监听Promise的状态变化

在我看来,这个是最重要的问题。因为虽然知道如何创建一个promise和改变它的状态很酷,但是如果我们不知道什么时候状态变化该做些什么,那么一切将毫无意义。

有一个我没提到的事情就是promise到底是什么。当你new一个Promise时,你确实只是创建了一个单纯的原始的JavaScript对象。这个对象能够触发两个方法,then和catch。这才是关键!当promise的状态转变为fulfilled时,then方法会被触发,当promise的状态转变为rejected时,catch方法会被触发。这意味着你创建了一个promise,同时你要设置当异步请求成功时你想运行的函数到then里,还有设置当异步请求失败时应该执行的函数到catch里。

让我们来看一个例子。我们会再次使用setTimeout在2秒后改变promise的状态为fulfilled。

如果你运行上面的代码,你会在大概2秒后在控制台看到“Success!”。原因有二,一是因为我们创建了promise,在大概2000毫秒后触发了resolve,这转变了promise的状态为fulfilled。二是,我们传递了onSuccess这个函数给promise的then方法。这样做的话,我们就相当于告诉promise在大概2秒后状态转换为fulfilled时去触发onSuccess。
现在我们假装有些不好的情况发生了,我们要去改变promise的状态为rejected。我们应该调用reject而不是resolve。

这次,我们调用reject的话,onSuccess函数没有被触发,取而代之的是onError函数会被触发。

现在,你了解了围绕Promise API的方法,让我们开始看下真实的代码。

记得我们看过的最后一个异步回调的例子吗?

我们能不能用Promise来改造这里的回调呢?我们该怎么把AJAX请求包裹进promise里?这样我们就能基于请求的走向来简单地执行resolve或reject了。让我们从getUser开始:

好的,注意getUser的参数已经发生变化了,现在只需接受id就好了。再也不需要另外的那两个回调函数了,我们也不会再失去控制权。取而代之的是,我们使用Promise的resolve和reject函数。如果请求成功了,resolve会被触发,如果失败的话,reject会被触发。

接下来我们重构getWeather。我们会采用同样的策略,使用resolve和reject来替换掉onSuccess和onFailure这两个回调函数:

看起来很棒。现在最后一件我们需要去更新的是我们的点击事件。记住,有以下几点我们需要去做:

  1. 从 GitHub API中取得用户信息;

  2. 根据用户位置从Yahoo Weather API获取天气信息;

  3. 根据用户信息和天气数据刷新UI。

让我们从第一点开始,从 GitHub API中取得用户信息:

注意,现在getUser返回给我们一个promise对象,能用来调用.then和.catch。如果取得了用户信息的话.then会被执行,如果取得了错误信息则.catch会被执行。

接下来来做第二点,根据用户位置从Yahoo Weather API获取天气信息:

这里我们采用了跟第一点同样的手法,并且这里要传递给它从userPromise那里得到的用户信息。

最后,根据用户信息和天气数据刷新UI:

咱们的新代码更好了,但还是有些待改进的问题。在咱们改进之前,有两个promise的特性你需要知晓,从resolve到then的链式调用和参数传递。

链式调用

.then和.catch都会返回一个promise对象。这看起来是个小细节,但其实这是非常重要的,因为这意味着promise能够被链式调用。

在下面的例子中,我们执行getPromise然后在约2000毫秒后会返回一个promise对象。然后,由于.then也会返回promise对象,所以我们能继续链接.then方法直到抛出错误被.catch捕获到:

酷!但为什么这会如此重要呢?回忆一下,回调函数会渐渐衰败的一个原因就是因为它脱离了你自然、有序的思维方式。当你把promise链接在一起,它没有跳脱你的思维方式,因为这些promise是按顺序链接的,运行getPromise之后运行then(logA)再运行then(logB)在然后……

再来看一个例子,当你使用fetch API时这里有一个普遍的用例。fetch会返回给你一个promise用来解析HTTP回应。为了得到实际的JSON数据,你会需要去调用.json,因为链式调用,我们能用顺序的方式来思考这个过程。

现在我们了解了链式调用,让我们来重构之前的getUser/getWeather代码:

参数传递

这看起来稍微好点了,但现在我们又面临了一个问题,你能点出来吗?在第二个.then里我们要去调用updateUI。问题是我们需要用户信息和天气信息,实际上我们只接收到了天气信息。我们得设法找出一种方法来让getWeather返回的promise包含用户信息和天气信息。

这里是关键。resolve只是一个函数,任何你传递给它的参数也会被传递到.then。这不就意味着在getWeather的内部,如果我们触发了resolve,我们能传递给它用户信息和天气信息。然后,在第二个.then方法里我们就能够接收到这些信息。

在咱们的点击事件里,你能看出promise和callbacks的差距:

这样的逻辑看起来会比较自然,因为符合我们通常的思维方式:getUser然后getWeather然后用数据更新UI。

Async/Await

现在,我们清楚地看到了promise迅速地拔高了我们异步代码的可读性,但是否还有能让它更好的方法呢?假设你是TC39委员会的一员,同时你有这个权利去给JavaScript语言增加新的特性。你会采取什么方法来改善这代码:

正如我们所讨论的,这代码读起来非常好。就如我们的脑子顺序化地工作着。但有一个问题是,运行这个代码我们需要把数据(用户信息)从第一个异步请求一路往下传递到最后一个.then。这不是什么大毛病,但这让我们不得不改变getWeather函数来适应传递用户信息数据。如果我们能够按同步代码那样的写法来写我们的异步代码呢?如果能做到的话,这个问题将迎刃而解,同时这样也保持了代码的可读性。这里有一个主意:

嗯,这样看起来很棒。咱们的异步代码看起来就是同步代码。我们的脑子可以毫不费力的接受它,因为我们已经非常熟悉这种思考方式了。遗憾的是,这样显然是运行不起来的。如你所知,上述代码中的用户信息和天气信息都只是从getUser和getWeather那返回的promise。但是记住,我们是TC39的一员,我们有所有的权力来添加任何我们想要添加的特性到语言上。诚然,要让这代码运行起来非常棘手。我们不得不去调试JavaScript引擎来分辨异步代码和同步代码。让我们给代码添加一些关键词使其更易理解。

首先,让我们给主函数体上添加一个关键字。这能引导引擎知晓在这个函数内部,我们有一些异步函数调用要执行。让我们使用async这个关键字:

酷。这看起来很合理。接下来让我们添加一个关键字让引擎知道哪个函数触发是异步的并且会返回一个promise,让我们使用await。就像,“嘿,引擎。这个函数是异步的并且会返回一个promise。不要用你那老套的处理法,你继续向下走同时等待这个promise的最终值,然后在继续之前返回它就可以了”。借助新的async和await关键字,我们的新代码看起来像是这样:

看起来很妙啊。我们创造了一个让异步代码样子和行为都很像同步代码的方法。接下来就是该如何去说服TC39的某些人使其知道这是个好主意。幸运的是,你大概猜到了,我们不需要去说服任何人,因为这个特性已经被JavaScript所支持,然后它叫做Async/Await。

async函数返回promise

现在你已经看到了Async/Await的好处了,让我们讨论一些我们需要知道的小细节。首先,只要你给一个函数添加async关键字,这个函数必然会返回一个promise。

虽然getPromise字面上是空函数,但它仍然会返回一个promise。

如果async函数返回一个值,这个值也会被包裹进一个promise里。这意味着你要用.then去得到它。

没有async的await是不允许的

如果你尝试在非async函数里使用await关键字,你会得到报错:

以下是我对此的看法。当你向函数添加async关键字时,它会做两件事。 它使得函数本身返回(或包裹得到的值返回)一个promise,并允许你可以在其中使用await。

错误处理

你可能注意到了,在我们原来的代码里,我们使用.catch方法来捕获错误信息。但当我们切换为Async/Await时,我们移除了这些代码。在Async/Await代码里,更普遍的做法是将你的代码包裹进try/catch代码块里来捕获错误:

Coin Marketplace

STEEM 0.28
TRX 0.13
JST 0.033
BTC 62916.93
ETH 3028.97
USDT 1.00
SBD 3.67