Async 与 Await 解读

前言

async/await产生原因:如果需要从多个数据库或者接口按顺序异步获取数据,最终会写出一坨纠缠不清的promise与回调。(鉴于本宝宝刚刚开始接触promise,这种糟心情况根本还没见识过……)

简单学习一下promise

一个promise抽象表达了一个非阻塞(阻塞指一个任务开始后,要等待该任务执行结果产生之后才继续执行后续任务)的异步流程,最典型的使用场景是网络或其他I/O操作(如读取一个文件或者发送一个HTTP请求)。promise用then方法来附加一个回调,用于执行该promise完成之后要做的事情。回调自身也可以返回一个promise,如此就可以将多个promise串联。

要使用promise,首先引入request-promise库:

1
var rp = require('request-promise');

(所以……更前一步是npm i下载个么?)

然后发送一个简单的HTTP GET请求并获得一个promise返回值:

1
2
3
4
5
6
console.log('Starting Execution');  // 第一处输出

const promise = rp('http://example.com/');
promise.then(result => console.log(result)); // 第二处输出

console.log("Can't know if promise has finished yet..."); // 第三处输出

在第一处输出后,第三处输出与这个get命令(虽然本宝宝不认识这个库的写法……)一起进行————“promise之后的代码跟promise自身是并发的”;直到这个get获得了(成功的)返回结果,再进行第二处输出。

*然后好欣慰,认出了ES6的箭头函数,虽然其实没有这么写的习惯……(跑题了)

JS中不存在一种方法可以让当前的执行流程阻塞直到promise完成,唯一可以用于提前计划promise完成后的执行逻辑的方式就是通过then附加回调函数。

然后如果这个请求失败了捏(比如网络异常了)?或者在then里面我们写错了什么导致了错误?这个时候就会转到catch里了~所以完整的可以这么写:

1
2
3
rp('http://example.com/').
then(() => console.log('Success')).
catch(e => console.log(`Failed: ${e}`))

*Promise.resolve、Promise.reject要去了解一下,返回一个假定肯定走成功或者失败路线的promise

纠在一起的组合promise

假设一个命题,请实现以下逻辑:

  • 发起一个HTTP请求,等待结果并将其输出
  • 再发起两个并发的HTTP请求
  • 当两个请求都完成时,一起输出他们

解答:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 第一次请求
const call1Promise = rp('http://example.com/');

call1Promise.then(result1 => {
// 请求成功了
console.log(result1);

// 再发两个请求
const call2Promise = rp('http://example.com/');
const call3Promise = rp('http://example.com/');

// Promise.all: 将这两个请求合并,要了解一下!
// 此处then的返回值还是个promise,所以后面可以再接个then
return Promise.all([call2Promise, call3Promise]);
}).then(arr => {
// 后两个promise都请求成功了
console.log(arr[0]);
console.log(arr[1]);
})

Async函数

async函数是定义返回promise的函数的简便写法。

1
2
3
4
5
6
7
8
// 成功的请求
async function asyncF() { function f() {
return 'TEST'; 〓 return Promise.resolve('TEST');
} }
// 失败的请求
async function asyncF() { function f() {
throw 'Error'; 〓 return Promise.reject('Error');
} }

Await

一个操作本身就是异步的(比如用promise包装的),它应该具备能力等待另一个异步操作先完成。但是JS解释器如何知道一个操作是不是在一个promise里的?

答案就是async关键字,所有的async函数一定会返回一个promise。所以,JS解释器也可以确信async函数里操作是用promise包装的异步过程。于是也就可以允许它等待其他promise。

键入await关键字,它只能在async函数内使用,让我们可以等待一个promise。如果在async函数外使用promise,我们依然需要使用then和回调函数:

1
2
3
4
5
6
7
8
async function f(){
// 内部的response请求成功后才会输出(这里的两句是不是可以看成阻塞了?)
const response = await rp('http://example.com/');
console.log(response);
}

// 在async函数外,所以要用then来处理请求成功后的输出
f().then(() => console.log('Finished'));

这时候上面那个问题可以这么来:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
async function solution() {
// 等待第一次请求完成并输出
console.log(await rp('http://example.com/'));

// 并发产生后两个请求,这里不等待,是异步的!
const call2Promise = rp('http://example.com/');
const call3Promise = rp('http://example.com/');

// 请求发出后,进行等待
const response2 = await call2Promise;
const response3 = await call3Promise;

console.log(response2);
console.log(response3);
}

// 调用async函数
solution().then(() => console.log('Finished'));

计算流程跟之前的图表描绘的一样,但是代码变得更加已读与直白。

事实上,async/await其实会翻译成promise与then回调(译者:babel其实是翻译成generator语法,再通过类似co的函数运行,co内部运行机制离不开promise)。每次我们使用await,解释器会创建一个promise然后把async函数的后续代码放到then回调里。(所以,前面call2Promise和call3Promise其实还是先等待到call2Promise再去等待call3Promise?等待的用词有点不太对可能)

错误处理

async函数如果await的对象失败了呢?这可以用try/catch处理:

1
2
3
4
5
6
7
async function f() {
try {
const promiseResult = await Promise.reject('Error');
} catch (e){
console.log(e);
}
}

一些补充

  • 作者的建议做法:把大部分的异步逻辑封装在一个或少量几个async函数里,然后在非async的代码区域里使用,这样就可以尽量减少书写then或catch回调。

  • 并发(concurrent)与并行(parallel)是有区别的:
    并发是组合多个独立过程来一起工作,并行是多个过程同时执行。
    并发是体现在应用的结构设计,并行是实际执行的方式。
    举个例子:
    将应用分割成多个线程是该应用并发模型的定义,将这些线程放到多个cpu核心上一起执行是确立它的并行。一个并发的系统也可以在一个单核处理器上正常运行,但这种情况并不是并行。

    恩,所以promise是一个将程序分解成多个并发的模块的工具,但不能保证在执行时是否并行,这要看解释器自身的实现(比如单线程的NodeJS去执行)。

致谢: