一、async/await的概念

ES7 新增了两个关键字: async和await,代表异步JavaScript编程范式的迁移。它改进了生成器的缺点,提供了在不阻塞主线程的情况下使用同步代码实现异步访问资源的能力。其实 async/await 是 Generator 的语法糖,它能实现的效果都能用then链来实现,它是为优化then链而开发出来的。

从字面上来看,async是“异步”的简写,await则为等待,所以 async 用来声明异步函数,这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力,使用await关键字可以暂停异步代码的执行,等待Promise解决。async 关键字可以让函数具有异步特征,但总体上代码仍然是同步求值的。

它们的用法很简单,首先用 async 关键字声明一个异步函数:

1
async function httpRequest() {}

然后就可以在这个函数内部使用 await 关键字了:

1
2
3
4
async function httpRequest() {
let res1 = await httpPromise(url1)
console.log(res1)
}

这里,await关键字会接收一个期约并将其转化为一个返回值或一个抛出的异常。通过情况下,我们不会使用await来接收一个保存期约的变量,更多的是把他放在一个会返回期约的函数调用面前,比如上述例子。这里的关键就是,await关键字并不会导致程序阻塞,代码仍然是异步的,而await只是掩盖了这个事实,这就意味着任何使用await的代码本身都是异步的。

下面来看看async函数返回了什么:

1
2
3
4
5
async function testAsy(){
return 'hello world';
}
let result = testAsy();
console.log(result)

可以看到,async 函数返回的是 Promise 对象。如果异步函数使用return关键字返回了值(如果没有return则会返回undefined),这个值则会被 Promise.resolve() 包装成 Promise 对象。异步函数始终返回Promise对象。

二、await到底在等啥?

一般我们认为 await 是在等待一个 async 函数完成。不过按语法说明,await 等待的是一个表达式,这个表达式的结果是 Promise 对象或其它值。

因为 async 函数返回一个 Promise 对象,所以 await 可以用于等待一个 async 函数的返回值——这也可以说是 await 在等 async 函数。但要清楚,它等的实际是一个返回值。注意,await 不仅用于等 Promise 对象,它可以等任意表达式的结果。所以,await 后面实际是可以接普通函数调用或者直接量的。所以下面这个示例完全可以正确运行:

1
2
3
4
5
6
7
8
9
10
11
12
function getSomething() {
return "something";
}
async function testAsync() {
return Promise.resolve("hello async");
}
async function test() {
const v1 = await getSomething();
const v2 = await testAsync();
console.log(v1, v2);
}
test(); // something hello async

await 表达式的运算结果取决于它等的是什么:

  • 如果它等到的不是一个 Promise 对象,那 await 表达式的运算结果就是它等到的内容;
  • 如果它等到的是一个 Promise 对象,await 就就会阻塞后面的代码,等着 Promise 对象 resolve,然后将得到的值作为 await 表达式的运算结果。

下面来看一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function testAsy(x){
return new Promise(resolve=>{setTimeout(() => {
resolve(x);
}, 3000)
}
)
}
async function testAwt(){
let result = await testAsy('hello world');
console.log(result); // 3秒钟之后出现hello world
console.log('cuger') // 3秒钟之后出现cug
}
testAwt();
console.log('cug') //立即输出cug

这就是 await 必须用在 async 函数中的原因。async 函数调用不会造成阻塞,它内部所有的阻塞都被封装在一个 Promise 对象中异步执行。await暂停当前async的执行,所以’cug’’最先输出,hello world’和 cuger 是3秒钟后同时出现的。

三、async/await的优势

async/await对比Promise的优势:

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的理解负担;
  • Promise传递中间值很麻烦,⽽async/await⼏乎是同步的写法,⾮常优雅;
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获比较冗余;
  • 调试友好,Promise的调试很差,由于没有代码块,不能在⼀个返回表达式的箭头函数中设置断点,如果在⼀个.then代码块中使⽤调试器的步进(step-over)功能,调试器并不会进⼊后续的.then代码块,因为调试器只能跟踪同步代码的每⼀步。

四、async/await的异常处理

利用 async/await 的语法糖,可以像处理同步代码的异常一样,来处理异步代码,这里还用上面的示例:

1
2
3
4
5
6
const exe = (flag) => () => new Promise((resolve, reject) => {
console.log(flag);
setTimeout(() => {
flag ? resolve("yes") : reject("no");
}, 1000);
});
1
2
3
4
5
6
7
8
9
10
const run = async () => {
try {
await exe(false)();
await exe(true)();
} catch (e) {
console.log(e);
}
}
run();
// 用try...catch来处理reject()

这里定义一个异步方法 run,由于 await 后面需要直接跟 Promise 对象,因此通过额外的一个方法调用符号 () 把原有的 exe 方法内部的 Thunk 包装拆掉,即执行 exe(false)() 或 exe(true)() 返回的就是 Promise 对象。在 try 块之后,使用 catch 来捕捉。运行代码会得到这样的输出:

1
2
false
no

这个 false 就是 exe 方法对入参的输出,而这个 no 就是 setTimeout 方法 reject 的回调返回,它通过异常捕获并最终在 catch 块中输出。就像我们所认识的同步代码一样,第四行的 exe(true) 并未得到执行。

五、其他细节问题

1、async函数在抛出返回值时,会根据返回值类型开启不同数目的微任务:

  • return结果值:非thenable、非promise(不等待)
  • return结果值:thenable(等待 1个then的时间)
  • return结果值:promise(等待 2个then的时间)

2、await右值类型区别:

  • 接非 thenable 类型,会立即向微任务队列添加一个微任务then,但不需等待

  • 接 thenable 类型,需要等待一个 then 的时间之后执行

  • 接Promise类型(有确定的返回值),会立即向微任务队列添加一个微任务then,但不需等待

    • TC 39 对await 后面是 promise 的情况如何处理进行了一次修改,移除了额外的两个微任务,在早期版本,依然会等待两个 then 的时间

六、总结

  1. async/await 是 Generator 的语法糖。

  2. async/await对比Promise的优势:

  • 代码读起来更加同步,Promise虽然摆脱了回调地狱,但是then的链式调⽤也会带来额外的理解负担;
  • 错误处理友好,async/await可以⽤成熟的try/catch,Promise的错误捕获比较冗余;
  1. 滥用 await 可能会导致性能问题,因为 await 会阻塞代码,也许之后的异步代码并不依赖于前者,但仍然需要等待前者完成,导致代码失去了并发性。