async/await

async

async是generater函数的语法糖,就是将 Generator 函数的星号*替换成async,将yield替换成await

async对generater函数的改进

  1. 内置执行器:不需要调用next(),就会自动执行
  2. 更好的语义:async表示函数里有异步操作,await表示紧跟在后面的表达式需要等待结果
  3. 更广的适用性:async函数的await命令后面,可以是 Promise 对象和原始类型的值(数值、字符串和布尔值,但这时会自动转成立即 resolved 的 Promise 对象)
  4. async函数的返回值是Promise:generater函数的返回值是遍历器对象

基本用法

async将你的函数返回值(自动地)转换为promise对象,不需要显式地返回promise对象

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

1
2
3
4
5
6
7
8
9
10
11
12
13
function sleep() {
return new Promise(resolve => {
setTimeout(() => {
console.log('finish')
resolve("sleep");
}, 2000);
});
}
async function test() {
let value = await sleep();
console.log("object");
}
test()

上面代码会先打印 finish 然后再打印 object 。因为 await 会等待 sleep 函数 resolve ,所以即使后面是同步代码,也不会先去执行同步代码再来执行异步代码。

async函数内部return语句返回的值,会成为then方法回调函数的参数

1
2
3
4
5
6
async function f() {
return 'hello world';
}

f().then(v => console.log(v))
// "hello world"

async函数内部抛出错误,会导致返回的 Promise 对象变为reject状态。抛出的错误对象会被catch方法回调函数接收到

1
2
3
4
5
6
7
8
9
async function f() {
throw new Error('出错了');
}

f().then(
v => console.log(v),
e => console.log(e)
)
// Error: 出错了

Promise 对象的状态变化

async函数返回的 Promise 对象,必须等到内部所有await命令后面的 Promise 对象执行完,才会发生状态改变,除非遇到return语句或者抛出错误。也就是说,只有async函数内部的异步操作执行完,才会执行then方法指定的回调函数。

1
2
3
4
5
6
async function getTitle(url) {
let response = await fetch(url);
let html = await response.text();
return html.match(/<title>([\s\S]+)<\/title>/i)[1];
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)

函数getTitle内部有三个操作:抓取网页、取出文本、匹配页面标题。只有这三个操作全部完成,才会执行then方法里面的console.log。

await 命令

正常情况下,await命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值。

1
2
3
4
5
6
7
8
async function f() {
// 等同于
// return 123;
return await 123;
}

f().then(v => console.log(v))
// 123

另一种情况是,await命令后面是一个thenable对象(即定义then方法的对象),那么await会将其等同于 Promise 对象。

await命令后面的 Promise 对象如果变为reject状态,则reject的参数会被catch方法的回调函数接收到

1
2
3
4
5
6
7
8
async function f() {
await Promise.reject('出错了');
}

f()
.then(v => console.log(v))
.catch(e => console.log(e))
// 出错了

注意,上面代码中,await语句前面没有return,但是reject方法的参数依然传入了catch方法的回调函数。这里如果在await前面加上return,效果是一样的。

任何一个await语句后面的 Promise 对象变为reject状态,那么整个async函数都会中断执行。

1
2
3
4
async function f() {
await Promise.reject('出错了');
await Promise.resolve('hello world'); // 不会执行
}

错误处理

有时,我们希望即使前一个异步操作失败,也不要中断后面的异步操作。这时可以将第一个await放在try…catch结构里面,这样不管这个异步操作是否成功,第二个await都会执行

1
2
3
4
5
6
7
8
9
10
11
async function f() {
try {
await Promise.reject('出错了');
} catch(e) {
}
return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// hello world

另一种方法是await后面的 Promise 对象再跟一个catch方法,处理前面可能出现的错误。

1
2
3
4
5
6
7
8
9
10
async function f() {
await Promise.reject('出错了')
.catch(e => console.log(e));
return await Promise.resolve('hello world');
}

f()
.then(v => console.log(v))
// 出错了
// hello world

独立的异步操作

独立的异步操作(即互不依赖),如果不存在继发关系,最好让它们同时触发。

1
2
let foo = await getFoo();
let bar = await getBar();

getFoo和getBar是两个独立的异步操作(即互不依赖),被写成继发关系。这样比较耗时,因为只有getFoo完成以后,才会执行getBar,完全可以让它们同时触发。

1
2
3
4
5
6
7
8
9
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()]);
// 或者for循环遍历

// 写法二
let fooPromise = getFoo();
let barPromise = getBar();
let foo = await fooPromise;
let bar = await barPromise;

await命令只能用在async函数之中,如果用在普通函数,就会报错。

for await…of

for await…of循环,则是用于遍历异步的 Iterator 接口

1
2
3
4
5
6
7
async function f() {
for await (const x of createAsyncIterable(['a', 'b'])) {
console.log(x);
}
}
// a
// b

createAsyncIterable()返回一个拥有异步遍历器接口的对象,for…of循环自动调用这个对象的异步遍历器的next方法,会得到一个 Promise 对象。await用来处理这个 Promise 对象,一旦resolve,就把得到的值(x)传入for…of的循环体。

如果next方法返回的 Promise 对象被reject,for await…of就会报错,要用try…catch捕捉。

1
2
3
4
5
6
7
8
9
async function () {
try {
for await (const x of createRejectingIterable()) {
console.log(x);
}
} catch (e) {
console.error(e);
}
}

for await…of循环也可以用于同步遍历器

1
2
3
4
5
6
7
(async function () {
for await (const x of ['a', 'b']) {
console.log(x);
}
})();
// a
// b

异步 Generator 函数

异步 Generator 函数的作用,是返回一个异步遍历器对象。

在语法上,异步 Generator 函数就是async函数与 Generator 函数的结合。

1
2
3
4
5
6
async function* gen() {
yield 'hello';
}
const genObj = gen();
genObj.next().then(x => console.log(x));
// { value: 'hello', done: false }

gen是一个异步 Generator 函数,执行后返回一个异步 Iterator 对象。对该对象调用next方法,返回一个 Promise 对象。

async与Event Loop

async函数返回一个 Promise 对象,可以使用then方法添加回调函数。当函数执行的时候,一旦遇到await就会先返回,等到异步操作完成,再接着执行函数体内后面的语句

  • await等待的是右侧的[表达式结果],如果右侧是一个函数,等待的是右侧函数的返回值,如果右侧是表达式则是右侧表达式的值
  • await在等待时会让出线程阻塞后面的执行。await的执行顺序为从右到左,会阻塞下面的代码执行,但并不是直接阻塞await的表达式。
  • await右侧如果不是promise,await会阻塞下面的代码,会先执行async外面的同步代码,等外面的同步代码执行完成在执行async中的代码。
  • 如果await右侧返回的是promise,await也会暂停async下面的代码,先执行async外面的同步代码,等着 Promise 对象 fulfilled,然后把 resolve 的参数作为 await 表达式的运算结果。
1
2
3
4
5
6
7
8
9
10
var a = 0
var b = async () => {
a = a + await 10
console.log('2', a) // -> '2' 10
a = (await 10) + a
console.log('3', a) // -> '3' 20
}
b()
a++
console.log('1', a) // -> '1' 1
  1. 首先函数 b 先执行,在执行到 await 10 之前变量 a 还是 0
  2. 因为在 await 内部实现了 generators,generators 会保留堆栈中东西,所以这时候 a = 0 被保存了下来
  3. 因为 await 是异步操作,遇到await就会立即返回一个pending状态的Promise对象
  4. 暂时返回执行代码的控制权,使得函数外的代码得以继续执行,所以会先执行 console.log(‘1’, a)
  5. 这时候同步代码执行完毕,开始执行异步代码,将保存下来的值拿出来使用,这时候 a = 10
  6. 然后后面就是常规执行代码了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
async function async1() {
console.log('async1')
await async2()
console.log('async1end')
}

async function async2() {
console.log('async2')
}

console.log('start')
setTimeout(function() {
console.log('setTimeout')
}, 0)

async1();

new Promise( function( resolve ) {
console.log('promise1')
resolve();
} ).then( function() {
console.log('promise2')
} )

console.log('end')

// start async1 async2 promise1 end promise2 async1end setTimeout
  1. js主线程走到并输出“start”
  2. 由于setTimeout执行的优先级低于主线程上的任务和Promise上的任务所以要等到最后
  3. 遇到async1(),输出“async1”,再执行async2()函数输出“async2”
  4. 由于走到async2()时遇到await,所以要跳出async1()函数体外
  5. 接着代码往下跑,执行new Promise对象,输出“promise1”
  6. Promise回调进入到微任务队列中,继续往下走,输出“script end”
  7. 现在回到async1()函数体内中的await async2()处,由于await后面接着async2()返回的是Promise对象还要再次跳出
    async1()函数体外继续执行以外的代码
  8. 这时候正好有Promise队列中的then需要执行,于是输出“promise2”
  9. setTimeout仍然靠后优先级低,再次回到async1()函数体内,接着执行输出“async1 end”
  10. 最后剩下主线程和Promise队列的任务都执行完成了,就输出“setTimeout”