为了帮助你了解一下历史的进程,我画了如下图来解释我们为啥要async和await

简介

async和await是为了简化Promise模式而被提出来的一种解决方案,所以为了学习async和await我们需要先明白Promise模式是啥。

Promise模式的提出是为了替代JS中本身的回调语法,那么为啥要替代回调语法呢?我们知道,JS一般的实现都是单线程的,所以我们都使用回调函数来进行操作的同步。但是,回调函数写起来实在是太丑了,一段使用回调函数的代码可能是这样的(出自http://callbackhell.com/):

因为写起来太难看了,而且理解起来也很不方便,所以人们就开始想办法解决回调函数的问题。此时,Promise模式被提出来了,要知道Promise模式是怎么替代回调函数的,参考如下代码:

最基本的回调函数方式

1
2
3
4
5
6
7
8
var fs = require("fs")

fs.readFile('input.txt', function (err, data) {
if (err) {
console.error(err)
}
console.log("read data:", data.toString());
})

改写为Promise模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
var fs = require("fs")

var readFile = function (file) {
// 定义一个Promise对象
// 参数是一个方法,方法需要两个参数
return new Promise(function (resolve, reject) {
fs.readFile(file, function (err, data) {
if (err) {
reject(err) // 错误时执行reject方法
}
resolve(data) // 执行resolve方法
});
})
}

// 获取Promise对象并且执行then方法
// then方法需要两个方法作为参数
readFile('input.txt').then(data => { // resolve 方法
console.log("promise read data:", data.toString())
}, err => { // reject 方法
console.error(err)
})

Promise模式

Promise类在nodejs中已经为我们构建好了(主流浏览器应该也都实现了这个类),所以我们可以直接使用。Promise的构造方法入参是一个方法,这个方法传入两个参数:resolve和reject。根据约定,当程序执行正常,方法resolve被执行;当程序出现异常,方法reject被执行。方法的执行条件已经定义完毕,下面就是方法体的定义了。方法readFile('input.txt')返回一个Promise对象,我们调用其 then 方法,这个方法传入两个方法。根据约定,第一个方法是为resolve方法的方法体,第二个方法是为reject方法的方法体。

也就是说,then 方法中的两个参数分别就是和Promise中传入的方法的两个参数相对应的。上面的resolve方法执行了,执行的就是then方法中的resolve方法方法体(参数也被正确的传递);上面的reject执行了,执行的就是下面reject方法方法体。

async and await

我们发现,Promise虽然在执行时避免了回调地狱,但是写这个 then 方法还是挺丑的,我们想要去掉这个 then。因此,async和await就出现了,它们的目的是当调用Promise对象的方法时候,能够简化操作。我们还是使用上面例子中的Promise对象的定义方式,而使用async和await来替代之前的Promise方法的执行方式,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
var fs = require("fs")

var readFile = function (file) {
// 定义一个Promise对象
// 参数是一个方法,方法需要两个参数
return new Promise(function (resolve, reject) {
fs.readFile(file, function (err, data) {
if (err) {
reject(err) // 错误时执行reject方法
}
resolve(data) // 执行resolve方法
});
})
}

var test = async (file) => {
try {
var data = await readFile(file) // 这里把resolve的入参返回给data
console.log('async and await:', data.toString())
} catch (err) {
console.error(err)
}
}
test('input.txt')

我们定义了一个async方法,在方法内使用await关键字修饰了一个Promise对象,这个修饰符将会把resolve对象执行时的入参返回,我们用data变量接受这个参数。仔细想想,我们的核心目的不就是接受到resolve方法和reject方法的参数吗?因为方法体其实都是我们自己定义的,如此一来拿到了resolve方法的入参,我们就可以用这个参数执行我们想要的操作了;同样的,根据约定,在async方法中reject方法一旦执行就会触发异常,所以我们用 try catch 包裹了代码快,catch的参数就是reject方法执行时候的入参。

仔细观察上面的代码,这样一来代码就非常的整洁且容易理解了。虽然我们增加了一个Promise对象定义的操作,但这个增加是值得的,因为我们大大的提高了代码的可读性。