关于async/await

高甜语法糖。
主要的工作是异步调用扁平化。
有一些前置的知识点,主要有两个,分别是 Promise 和 Generator 函数。

参考连接/书籍

《ES6 标准入门》

有了 async/await,你可以丢掉 promise 链了
async function
理解 JavaScript 的 async/await

一些说明

async 函数是最新的异步函数问题的解决方案。
至于什么是异步函数的问题,主要就是解决回调地狱,实现异步函数扁平化。
JavaScript 的异步方案:回调 -> Promise -> Generator -> async/await

跟 Promise 的关系?
最主要的关系是 async 函数的返回值是一个 Promise 对象。
当然 await 等待的表达式也有可能是一个 Promise。

进一步说,async 函数完全可以看作由多个异步操作包装成的一个 Promise 对象,而 await 命令就是内部 then 命令的语法糖。 --《ES6 标准入门》

跟 Generator 的关系?
严格来说其实 async 是 Generator 的语法糖。

什么是 async ?
"异步"的简写。

什么是 await ?
有说法说是 async wait 的简写。
还需要注意的是 await 只能在 async 函数里使用。

由此应该很好理解 async 用来申明一个异步函数,await 就用来等待一个异步操作的完成。那么理解下面这句 MDN 上的话也就不难了。

async/await 的目的是简化使用多个 promise 时的同步行为,并对一组 Promises 执行某些操作。正如 Promises 类似于结构化回调,async/await 更像结合了 generators 和 promises。

与 Generator 的关系

async 和 Generator 的区别就是 async 使用起来更可口好用而已。
但是还是有一些差异的。

  1. async 自带执行器
    区别就是 Generator 需要不断的 next() 或者使用 co 模块。
    但是 async 自带了执行器,所以调用一次 async 函数,就会自动执行,输出最终的结果。
  2. 语义化
    语法糖嘛,就是体现在这里。
    主要就是把 Generator 的星号(*)替换成了 async 关键字,把 yield 关键字替换成了 await 关键字。
  3. 返回值
    async 函数的返回值是一个 promise

    返回的 Promise 对象会运行执行(resolve)异步函数的返回结果,或者运行拒绝(reject)——如果异步函数抛出异常的话。-- MDN

  4. yield 的扩充
    虽然说 async 是把 yield 关键字用 await 代替。
    但是 co 模块约定 yield 之后只能是 Thunk 或者 Promise 对象,但是 await 之后可以是 Promise 以及原始类型的值。(其实如果是原始类型,也会被转换成 resolve 的 Promise

await 在等什么

hmmm 在看《ES6 标准入门的》的时候,有如下这一句话

当函数执行的时候,一旦遇到 await 就会先返回,等到异步操作完成,再接着执行函数体内后面的语句。

其实真的不知道"先返回"是什么意思啊喂。

1
2
3
4
5
async function demo() {
let a = await "123";
let b = await 12;
}
demo(); // Promise {<resolved>: undefined}

上面的例子里,和预期的一样,返回了一个 Promise,但是完完全全没有在后续里有提到 a / b 这俩变量(当然啦,要是被 return 了就另说)。
所以我猜测书里说的先返回,是会先等待 await 关键字之后的异步操作的返回,再接着执行之后的语句。

所以 await 等的就是之后跟在其后的异步的表达式。

至于为什么 await 之后是表达式(其实不重要啦),await 之后可以是 Promise 以及原始类型,但是如果不是 Promise 对象,就会被转化成一个立即 resolve 的 Promise 对象。

那么等待的内容是非 Promise 对象还好说,就是那个东西。
如果等到的内容是一个 Promise,await 会阻塞 await 语句后面的代码,直到 Promise 的状态变成 resolve,然后得到 resolve 的值,作为 await 表达式的结果。

错误处理

提到了如果等到的内容是一个 Promise,最后会将 resolve 的结果作为 await 表达式的值。
那么作为一个 Promise,成功也有可能失败嘛,总有可能结果是 reject。

如果结果是 reject,那么整个 async 函数会直接中断。

可以用 try / catch 包裹 await 语句,这样无论失败与否都会执行之后的语句。
还可以使用 .catch() 来捕获错误,因为其实也提到了就算 await 表达式是一个原始类型,也会被弄成立即 resolve 的 Promise,所以放心大胆的 .catch() 就好了。

以及因为 async 返回的也是一个 Promise 对象,所以对这个 Promise 进行 .catch() 捕获错误也是可以捕获到的,但是并没有解决"遇到一个 await 的结果是 reject 之后的代码都不会执行"的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
async function f() {
await new Promise((resolve, reject) => {
throw new Error("出错了");
});
return await 123;
}
f()
.then(v => console.log(v))
.catch(e => console.log(e));
// VM1360:1 Error: 出错了
// at <anonymous>:3:15
// at new Promise (<anonymous>)
// at f (<anonymous>:2:11)
// at <anonymous>:1:1

上面的例子中,虽然 f() 有设置 return,但还是只打印了错误信息,并没有打印 123,可见遇到了 reject 之后,之后的代码并没有执行。

2019-09-30更新

同时触发

这里主要是面向优化。
而且是《ES6 标准入门》里提到的,有一些原理并没有很懂。

需要先明确一点,async 函数说浅了也就是一个返回 Promise 的函数,而往深了说,Promise 是解决回调地狱,async 函数就是解决 Promise 的 .then() 链。

单一的 Promise 链并不能发现 async/await 的优势,但是,如果需要处理由多个 Promise 组成的 then 链的时候,优势就能体现出来了(很有意思,Promise 通过 then 链来解决多层回调的问题,现在又用 async/await 来进一步优化它)。

既然是面向解决多个 Promise 的问题,有多个异步操作需要进行的话

多个 await 命令后面的异步操作如果不存在继发关系,最好让他们同时触发。

举个 《ES6 标准入门》里的例子

1
2
3
4
5
6
7
8
9
10
11
12
// 假设是在 async 函数环境下
let foo = await getFoo();
let bar = await getBar();
// 上面的代码中,getFoo & getBar 是两个独立的异步操作,相互不依赖,但是写成了继发执行
// 可以使用如下两种方式实现同时触发(并发执行)
// 方法 1
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 方法 2
let fooPromise = getFoo();
let barPromise = geetBar();
let foo = await fooPromise;
let bar = await barPromise;

有一说一,确实不知道原理是什么。

并发和继发

继发执行有点顺序执行的感觉,后面的程序运行对前面的代码有所依赖。

继发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 比较常见的写法是继发执行
async function getData() {
var res1 = await fetch(url1);
var res2 = await fetch(url2);
var res3 = await fetch(url3);
return "all data got";
}
// for of 也是继发执行
async function dbFuc(db) {
let docs = [{}, {}, {}];
for (let doc of docs) {
await db.post(doc);
}
}

并发执行有利于提高效率,但是有前提条件就是几个异步代码需要是相互独立的。

并发
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 比较常见的并发就是 Promise.all
async function loadData() {
let res = awaitPromise.all([fetch(url1), fetch(url2), fetch(url3)]);
return "all data got";
}
// 还有就是使用 map / forEach
// 也就是说 map 和 forEach 函数是并发执行的
async function loadData(urls) {
// 并发读取 url
const textPromises = urls.map(async url => {
const response = await fetch(url);
return response.text();
});

// 按次序输出
for (const textPromise of textPromises) {
console.log(await textPromise);
}
}
0%