前言

在做项目的过程中,凡是调用接口都使用了async await,只知道这是用来解决异步的,对其原理却一窍不通,特此写下这篇文章,从回调地狱说起,来揭秘async await的前世今生。

起源

首先要明确的是,js是一个单线程语言。为了避免个别耗时过多的任务造成阻塞,因而划分出了同步任务与异步任务,并且js中正常的函数执行是同步执行的(即按顺序依次执行)。典型的异步函数有setTimeout(), 如下 :

1
2
3
4
setTimeout(function () {
console.log("First");
}, 1000);
console.log("second")

上面的这段函数的输出会先输出”second”,而不是按顺序执行,因为这里的setTimeout()为异步函数,程序执行到异步函数时,会先不进入主线程、而进入任务队列(task queue)的任务,在引擎认为此任务可以执行了(这里是等待一秒后),才会进入主线程执行。

这里的异步任务执行时间很明确,即在等待1秒后。但有些异步函数的具体执行结束时间是不能确定的,比如读取文件(花费时间和文件大小有关),比如调用接口(与后台性能甚至网速有关)。

如果说,我们需要异步任务的结果来干一些事,此时该如何确定什么时候能拿到异步任务的结果 ?

答案是用回调函数。回调函数 : 将函数作为参数传给另外一个函数。

下面以setTimeout()为异步函数为例。

1
2
3
4
5
6
7
8
9
10
let a = 0,b = 1
let c = 100
// function (){c = a + b ;console.log('c1:',c)}就是回调函数,它只有在1秒后才会执行
setTimeout(function (){
c = a + b
console.log('c1:',c) //c1:1
},5000);
console.log('c:',c) //c:100

//先输出c,一秒后输出c1

如果有多个异步函数依次调用呢?就要写成这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
let a = 0,b = 1
let c = 100
let d = 1
// function (){c = a + b ;console.log('c1:',c)}就是回调函数,它只有在1秒后才会执行
setTimeout(function (){
c = a + b
console.log('c1:',c)
setTimeout(function(){
d = c * 2
console.log('d:',d) //d:2
},1000)
},5000);

//等待5秒输出c,再等待一秒后输出d

为了完成 回调的回调,上面的代码就写成了“瀑布式”,代码冗余,缩进繁琐,难以维护,若有10个异步函数需要依次执行,就形成了 回调地狱

总结一下,回调地狱就是为是实现代码顺序执行而出现的一种操作,但它会造成我们的代码可读性非常差,后期不好维护

Promise

Promise 是一个 ECMAScript 6 提供的类,目的是更加优雅地书写复杂的异步任务。下面是Promise对象:

1
2
3
new Promise(function (resolve, reject) {
// 要做的事情...
});

首先可以看到Promise构造函数有两个参数:reslovedrejected,他们都是回调函数。

  • resloved函数:异步操作成功后调用,并将异步操作的结果传递出去用, 用 .then(): 接收
  • rejected函数:异步操作失败后调用,将失败的信息传递出去, 用.catch(): 接收
  • Promise 状态发生改变,就会触发.then()里的响应函数处理后续步骤.

为了避免回调地狱,现在我们用 Promise 来实现同样的功能:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
new Promise(function (resolve, reject) {
setTimeout(function () {
c = a + b
console.log('c1:',c)
resolve();//这里代表成功调用了,如果没有这个resolve(),下面的.then()永远不会调用
//也可以写作rejected(),下面用.catch()接收,代表错误情况
}, 5000);
}).then(function () {
return new Promise(function (resolve, reject) {
setTimeout(function () {
d = c * 2
console.log('d:',d)
resolve();
}, 1000);
});
})

上述代码已经解决了回调地狱问题(将嵌套格式的代码变成了顺序格式的代码),为了更简洁的代码可以写成Promise函数和调用两个部分:

Promise函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//这种返回值为一个 Promise 对象的函数称作 Promise 函数,它常常用于开发基于异步操作的库。
function add(delay,num1,num2){
return new Promise(function (resolve, reject) {
setTimeout(function () {
c = num1 + num2
console.log('c1:',c)
resolve();
}, delay);
})
}
function multi(delay,num1){
return new Promise(function (resolve, reject) {
setTimeout(function () {
d = num1 * 2
console.log('d:',d)
resolve();
}, delay);
})
}

调用:

1
2
3
4
//简洁多了
add(5000, a, b).then(function() {
multi(1000,c)
})

async await

想要继续优化上面的代码,就引出了异步函数(async function)。

ECMAScript 7提供了async/await来编写异步代码,是回调地狱的终极解决方案,使得异步代码看起来像同步代码,这正是它的魔力所在。。

对于上文编写的两个Promise函数add与multi,可以通过async await调用

1
2
3
4
5
async function start(){
await add(5000, a, b)
await multi(1000,c)
}
start()

注意 :async/await是基于Promise实现的,它不能用于普通的回调函数。

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

实现你的async await

敬请期待 ….