Fork me on GitHub

ES6之Async/Await

在前面的两篇博客中,我们介绍了ES6promisegenerator,它们都是用来解决 js中的异步操作问题的。本篇博客的主角,async函数,同样是用于解决异步回调问题,它甚至被称为异步操作的终极解决方案。使用它,我们可以像处理简单的同步操作一样来处理一些异步操作,彻底告别回调函数方式的回调地狱问题,promise方法绵长的then调用问题,generator函数需要引入 co 库等这些异步处理方案中恼人的不完美之处。

准确的来说,async函数其实是 ES7 中的新语言特性,但babel已经完全提供了它的转码,所以在可以使用 ES6的地方,我们都可以大胆的使用 async函数。

为了图个方便,就将此篇博客无伤大雅的放在 ES6 系列中,所以特此说明。

好了,下面就来具体介绍以下我们今天的主角吧。

基本概念

async函数,其实是将promisegenerator函数结合在一起,从而将异步处理步骤大大简化的一种函数。

首先,我们来复习一下系列博客的前两篇中介绍的generatorpromise

generator函数,通过为函数定义添加 * 号来声明,通过yield关键字,得到异步结果,并在我们使用next执行器时,将异步结果传递给我们。它的缺点在于,我们必须在异步处理完成时,调用next执行器来进行结果传递,这意味着,我们必须对一些异步 API进行二次包装,增加了异步处理的复杂性。虽然 co库为我们提供了类似自动执行器的功能,但要求异步操作必须返回一个thunk函数或promise,在实际使用中仍然是比较麻烦的。具体见ES6之generator

promise,将回调函数规范化,通过 resolve传递异步操作结果,通过reject传递异步操作异常,通过then的链式调用,来将异步操作序列化。但在一些复杂的异步操作时,new Promise(function(resolve,reject){...}),这种冗长的写法,以及大串的then调用,也会使代码混乱和难以维护。具体见ES6之promise

async函数,作为generator函数的语法糖,将generatoryield机制,通过包装为promise实现自动执行,从而避免了我们手动使用next执行器或者使用一些类似co库这种自动执行器的麻烦。

Async

首先来看一下async函数的基本语法:

JavaScript
1
2
3
4
//通过在函数声明前加上 async,来声明函数是一个async函数。
async function foo(){
return 'this is a async func'
}

简单的在函数前加上async关键字,就声明了函数 foo 为一个异步的 async函数。我们可以像调用一个普通函数一样调用它。

但特别之处在于,这个函数并不会像普通函数一样简单的返回我们return的值,它总是将我们在函数中return的值转化为一个promise对象返回。

JavaScript
1
2
3
4
5
6
7
console.log(foo())
//Promise{<resolved>: "this is a async func"}

foo().then(res => {
console.log(res)
})
//this is a async func

通过上面的代码,我们可以清楚的看出async函数总是返回一个promise对象。还记得我们在promise中提到的resolve实例方法吗?async就是使用它,来将我们在函数内部声明的返回值包装为promise对象的。

当然,如果我们不在async函数中显式返回任何值,那么它就会返回 Promise.resove(undefined)

Await

async函数当然不仅仅只是返回一个promise对象这么简单,通过Await关键字,才能真正体现出async无阻塞,以同步方式来书写异步代码的魔力所在。

await关键字,用在async函数中,从名字也可以看出,它用于等待一个异步操作的完成并得到异步操作的结果。类似yield关键字只能用在generator函数中,await也只能用在async中。

await关键字,会等待跟随在它后面的表达式完成,取得表达式的值。如果它后面跟随的表达式返回一个promise对象,它就会执行这个promise对象的then方法,得到promiseresolved值。

通过代码来看一下它的具体作用:

JavaScript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
//我们定义一个返回promise对象的异步函数
function time() {
return new Promise((resolve, reject) => {
setTimeout(function(){
resolve(100)
},3000)
})
}

async function bar(){
await val = time()
return val+100
}

//别忘了我们上面提到的,async永远返回一个promise对象
bar()
//Promise { <state>: "pending" }

//3秒后我们得到异步的结果值
bar.then(res=>res)
//200

从上面的代码我们可以看出,await关键字,将其后跟随的表达式进行解析,如果是promise对象,就会等待promise对象转换为fullfilled状态,得到其resolve出来的值。

await简洁直接的解决了我们调用异步操作并得到异步操作结果这个过程的麻烦,在遇到异步操作的处理时,我们只需要简单的在其前面加上await,它会去执行这个异步操作,等待着得到异步的结果,从而让我们的代码逻辑清晰,再也不需要那一长条的then链了。

另外,await后面如果跟随的是一个同步操作,它也会直接返回这个同步操作的结果值,跟不加await是一样的。这也解决了我们使用generator时,yield必须返回一个thunk函数或promise对象才能被co库自动执行的问题。

异常处理

async/await,已经算是比较完美的异步处理方案了。但它仍然有美中不足的地方,那就是异常处理。

我们知道,promise会有两种状态,fullfilledrejected,当异步操作发生错误时,promise会进入rejected状态,传递出错误。但是在async函数中,await只会去自动获取其得到的promiseresolve值,并不关心reject值。这也就意味着,我们必须自己去处理异步操作返回promiserejected状态。

首先,第一种方法,直接了当,我们在await后面的promise对象上直接处理reject。代码形如下:

JavaScript
1
2
3
4
5
async function foo() {
let a = await fetch('a').catch(err => {console.log(err)})
let b = await fetch('b').catch(err => {console.log(err)})
return a+b
}

但是上面这种方法,在async包装了很多异步操作时,有很多await时,需要为每个await后面的异步操作都加上catch ,显得臃肿和累赘,非常不优雅。

第二种方法,就是使用try/catch,将多个异步操作的await都放在try块中,并在catch块中统一处理。

JavaScript
1
2
3
4
5
6
7
8
async function foo() {
try {
let a = await fetch('a')
let b = await fetch('b')
} catch(err) {
console.log(err)
}
}

这种方法,算是在实际应用中使用的较多的一种方法,它能够方便的同时处理同步和异步错误。但是在异步操作逻辑比较复杂时,也并不是那么的优雅,会一定程度增加代码的不可读性和复杂度。

总结

关于await/async函数的介绍,就到这里。其实如果仔细的了解了generator函数和promiseasync函数是非常容易理解的,也可以很直观的体会到它相比于其他两者的简洁优雅。作为 JavaScript 近年来推出的最革命性的特性之一,相信在后面的前端领域发展中,它也会发挥越来越重要的作用。

好啦,这篇博客就到这里。谢谢阅读,鞠躬退场^O^。

----本文结束感谢阅读----