关于 JavaScript 异步的奇妙进化史

人的一生,大概是从稚嫩到复杂,而最终是否归于佛系简单,因人修炼而异,JavaScript 语言亦是如此。

网页从静态到动态的变迁

几十年前的导航网站,清爽又简单,没有什么特别的功能,只是单纯的展示,现成的网页在服务器上静静躺着,高效毫无压力,让人很喜欢。

几十年后的今天,静态页面远不能满足用户的需求,网站变得复杂起来,用户交互越来越频繁,从而产生大量复杂的内部交互,为了解决这种复杂,出现了各种系统“模式”,从而很容易的在外部获取数据,并实时展示给用户。

获取外部数据实际上就是“网络调用”,这个时候“异步”这个词汇出现了。

编程语言中的异步

计算机在设计上是异步的。异步意味着某些事情可以独立于主程序流而发生。在当前的消费者计算机中,每个程序都运行在一个特定的时间段,然后它停止执行,以让另一个程序继续执行。这种循环运行如此之快以至于我们无法注意到,我们认为我们的计算机同时运行许多程序,但这是一种幻觉(除了多处理器机器)。程序内部使用 中断 ,这是一个发送到处理器的信号,以引起系统的注意。

JavaScript 语言默认是同步的,并且是单线程的。JS代码无法创建新线程,这意味着无法并行运行。

代码是一个接一个地串行执行,我们的 JavaScript 诞生于浏览器中,一开始其主要工作就是响应用户操作,如 onClick,onMouseOver,onChange,onSubmit等。 怎么用同步编程模型呢?

答案是在它的环境中。 浏览器通过提供一组可以处理这种功能的 API 来解决这个问题。

什么是函数?

在我刚开始学习编程的时候,我把函数看成一台机器。这些机器可以做任何你想让他们完成的工作。甚至是接收一个输入然后返回一个值。每台机器都有一个按钮,你可以在你想让他们运转的时候按下这个按钮,在函数里,也就是()。

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赋值给了不同的变量me、you和someoneElse,值得注意的是,原函数add和我们定义的三个变量都指向的是同一块内存。实际上它们是同一个东西,只不过有不同的名字。因此当我们调用me、you和someoneElse时,就好像我们在调用add一样。

回调函数对异步编程的实现

所谓回调函数(Callbacks),就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。它的英语名字callback,直译过来就是"重新调用"。 它的英语名字callback,直译过来就是”重新调用”。

举个例子

你无法知道用户何时要单击按钮,因此你要为click事件定义事件处理程序。此事件处理程序接受一个函数,该函数将在事件触发时调用:

// 例子 1
document.getElementById('button').addEventListener('click', () => {
add(2,3);
})

这就是所谓的回调函数。

细致剖解回调函数

如果上面的例子还没懂?那么我们继续用刚刚的 add 函数讲讲。

现在,要是我们把 add 这台机器传入到其他机器中去会发生什么?记住,谁按下了()按钮并不重要,重要的是只要按钮被按下,机器就会运行起来。

// 例子 2
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 (回调函数),而接收「函数参数」的函数称之为 高阶函数。

所以常常把函数比喻成 JavaScript 的 “一等公民” ,是因为它可以分配给变量并传递给其他函数。

例子 1 中 addEventListener 就高阶函数。例子 2 中 addFive 就是高阶函数。

为了体现语义化的重要性,我们将上面的代码重新命名,用例子 3 来表示这个概念:

// 例子 3
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的元素被点击了你就帮我执行它」。

回调在任何地方都使用,定时器也是最经典的:

setTimeout(() => {
// 2 秒后做一些事情
}, 2000)

现在,我们仅仅看到了同步执行的示例。但正如我们在文章开头时说到的:我们开发的大多数应用中都不具备其需要的所有数据。当用户与我们的应用进行交互时,应用需要获取外部数据。由此我们已经看到了 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
})
})

你可能已经发现,这里已经添加了很多层回调函数,首先我们告诉程序在 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)
})

OK,函数名帮助我们理解了到底发生了什么。但是讲真的,问题真的解决了吗?并没有。我们仅仅是解决了回调地狱中可读性的问题。即使是写成了单独的函数,我们顺序思考的天性依然被层层嵌套打断了。
回调函数的下一个问题就要提到「控制反转」了。你写下一个回调函数,假设你将回调函数传给的那个程序是可靠的,能在它该调用的时候进行调用。这实际上就是将程序的控制权转交给另一个程序。
当你使用类似 jQuery 或 loadsh 等第三方库,甚至是原生 JS 时,回调函数会在正确的时间使用正确的参数被调用的假设是合理的。然而,回调函数是你与大多数第三方库交互的接口,不管是有意还是无意,第三方库都有中断与回调函数交互的可能。

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

由于你并不是唯一调用criticalFunction的那一个,你对参数的调用完全没有控制权。大多数时候这都不是个问题,如果是的话,那问题可就大了。

参考链接

Promise


你有没有过未曾预约的情况下去一个非常火爆的餐厅吃饭?遇到这种情况时,餐厅会在有空位的时候通过某种方式联系你。一种古老的方式就是服务人员会记下你的名字,在有空位的情况下喊你。随着时代的进步,他们也开发出了新花样,记下你的电话号码以备有空位时短信通知你,这就允许你不用在餐厅门口死守了,还有最重要的一点就是他们可以在任何时候往你的手机推送广告。

听起来熟悉不?这就是 callbacks 的一种比喻啊。就像把一个回调函数传给第三方服务一样,我们把自己的号码传给了餐厅。你期望的是餐厅有空位时联系你,就像你期望第三方服务在某个时刻以某种方式调用你的回调函数一样。一旦你的号码或者说回调函数落在他们手中,你就对其失去了控制。

幸运的是,现在有了另一种解决方案。这种设计方案允许你保留所有控制权。你可能之前已经体验过了,餐厅可能会给你一个蜂鸣器,类似这种:

你没用过也没关系,它的设计思路很简单。这回餐馆不需要记下你的名字或者电话了,取而代之给你一个这样的装置。在该装置开始蜂鸣或者闪光时,就说明餐厅有空位了。在你等位的时候依然可以做任何你想做的事,而控制权还留在你手中。而且情况恰好跟之前相反,餐厅反而要给你掌握控制权的东西。从而不必控制反转了。

这个蜂鸣器会一直处于三个不同状态之一下 —— 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)
})

具体改变过程看下面的过程:

可以看到 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);

Generator

我们需要明确一个问题,在JavaScript中,任何一个函数只要开始执行,便无法停止下来直到执行完成,但是 Generator 提供这种能力,因此有人就利用这个特性来处理异步程序。

但是 Generator 的主要用途并不是拿来处理异步,所以这里就忽略了。

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

代码简练

使用async / await明显节约了不少代码。我们不需要写.then,不需要写匿名函数处理Promiseresolve值,也不需要定义多余的data变量,还避免了嵌套代码。这些小的优点会迅速累计起来,这在之后的代码示例中会更加明显。

原文基础上有改动,作为笔记

作者:zhaofeihao
链接:https://juejin.im/post/5c9c8861e51d4551a43f84fb

坚持原创技术分享,您的支持将鼓励我继续创作!