• 异步函数,也称为“async/await”(语法关键字)。
  • ES8的async/await旨在解决利用异步结构组织代码的问题。

异步函数

1、async

  • async关键字用于声明异步函数。这个关键字可以用在函数声明、函数表达式、箭头函数和方法上。
1
2
3
4
5
6
async function foo() {} 
let bar = async function() {};
let baz = async () => {};
class Qux {
async qux() {}
}
  • 使用async关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的。
  • 在参数或闭包方面,异步函数仍然具有普通JavaScript函数的正常行为。
1
2
3
4
5
6
7
async function foo() { 
console.log(1);
}
foo();
console.log(2);
// 1
// 2
  • 异步函数如果使用return关键字返回了值(如果没有return则会返回undefined),这个值会被Promise.resolve()包装成一个期约对象。
  • 异步函数始终返回期约对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function foo() { 
console.log(1);
return 3;
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1 => 异步函数中输出
// 2 => 非重入期约方法优先执行
// 3 => 排队等待最后输出
//============等价于===============
async function foo() {
console.log(1);
return Promise.resolve(3);
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3
  • 异步函数的返回值期待(但实际上并不要求)一个实现thenable接口的对象,但常规的值也可以。
  • 如果返回的是实现thenable接口的对象,则这个对象可以由提供给then()的处理程序“解包”。
  • 如果不是,则返回值就被当作已经解决的期约。
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 foo() {
return 'foo';
}
foo().then(console.log);
// foo
// 返回一个没有实现 thenable 接口的对象
async function bar() {
return ['bar'];
}
bar().then(console.log);
// ['bar']
// 返回一个实现了 thenable 接口的非期约对象
async function baz() {
const thenable = {
then(callback) { callback('baz'); }
};
return thenable;
}
baz().then(console.log);
// baz
// 返回一个期约
async function qux() {
return Promise.resolve('qux');
}
qux().then(console.log);
// qux
  • 与在期约处理程序中一样,在异步函数中抛出错误会返回拒绝的期约。拒绝期约的错误不会被异步函数捕获
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
async function foo() { 
console.log(1);
throw 3;
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

async function foo() {
console.log(1);
Promise.reject(3);
}
// Attach a rejected handler to the returned promise
foo().catch(console.log);
console.log(2);
// 1
// 2
// Uncaught (in promise): 3

2、await

  • 使用await关键字可以暂停异步函数代码的执行,等待期约解决
  • await关键字会暂停执行异步函数后面的代码,让出 JavaScript运行时的执行线程
  • await关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 异步打印"foo" 
async function foo() {
console.log(await Promise.resolve('foo'));
}
foo();
// foo
// 异步打印"bar"
async function bar() {
return await Promise.resolve('bar');
}
bar().then(console.log);
// bar
// 1000 毫秒后异步打印"baz"
async function baz() {
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log('baz');
}
baz();
// baz(1000 毫秒后)
  • await关键字期待(但实际上并不要求)一个实现thenable接口的对象,但常规的值也可以。
  • 如果是实现thenable接口的对象,则这个对象可以由await来“解包”。
  • 如果不是,则这个值就被当作已经解决的期约。
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 foo() {
console.log(await 'foo');
}
foo();
// foo
// 等待一个没有实现 thenable 接口的对象
async function bar() {
console.log(await ['bar']);
}
bar();
// ['bar']
// 等待一个实现了 thenable 接口的非期约对象
async function baz() {
const thenable = {
then(callback) { callback('baz'); }
};
console.log(await thenable);
}
baz();
// baz
// 等待一个期约
async function qux() {
console.log(await Promise.resolve('qux'));
}
qux();
// qux
  • 等待会抛出错误的同步操作,会返回拒绝的期约。
1
2
3
4
5
6
7
8
9
10
async function foo() { 
console.log(1);
await (() => { throw 3; })();
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
  • 单独的 Promise.reject()不会被异步函数捕获,而会抛出未捕获错误。不过,对拒绝的期约使用await 则会释放( unwrap)错误值(将拒绝期约返回)。
1
2
3
4
5
6
7
8
9
10
11
async function foo() { 
console.log(1);
await Promise.reject(3);
console.log(4); // 这行代码不会执行
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3

3、await的限制

  • await 关键字必须在异步函数中使用,不能在顶级上下文如script标签或模块中使用。
1
2
3
4
5
6
7
8
9
10
async function foo() { 
console.log(await Promise.resolve(3));
}
foo();
// 3
// 立即调用的异步函数表达式
(async function() {
console.log(await Promise.resolve(3));
})();
// 3
  • 异步函数的特质不会扩展到嵌套函数。
  • await关键字只能直接出现在异步函数的定义中。
  • 在同步函数内部使用await会抛出SyntaxError。
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
// 不允许:await 出现在了箭头函数中
function foo() {
const syncFn = () => {
return await Promise.resolve('foo');
};
console.log(syncFn());
}
// 不允许:await 出现在了同步函数声明中
function bar() {
function syncFn() {
return await Promise.resolve('bar');
}
console.log(syncFn());
}
// 不允许:await 出现在了同步函数表达式中
function baz() {
const syncFn = function() {
return await Promise.resolve('baz');
};
console.log(syncFn());
}
// 不允许:IIFE 使用同步函数表达式或箭头函数
function qux() {
(function () { console.log(await Promise.resolve('qux')); })();
(() => console.log(await Promise.resolve('qux')))();
}

停止和恢复执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
async function foo() { 
console.log(await Promise.resolve('foo'));
}
async function bar() {
console.log(await 'bar');
}
async function baz() {
console.log('baz');
}
foo();
bar();
baz();
// baz
// bar
// foo
  • async/await中真正起作用的是 await。
  • async关键字,无论从哪方面来看,都不过是一个标识符。
  • 异步函数如果不包含await关键字,其执行基本上跟普通函数没有什么区别。
1
2
3
4
5
6
7
8
9
async function foo() { 
console.log(2);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3
  • JavaScript运行时在碰到await关键字时,会记录在哪里暂停执行。等到await 右边的值可用了,JavaScript运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
  • 即使await后面跟着一个立即可用的值,函数的其余部分也会被异步求值。
1
2
3
4
5
6
7
8
9
10
11
12
async function foo() { 
console.log(2);
await null;
console.log(4);
}
console.log(1);
foo();
console.log(3);
// 1
// 2
// 3
// 4

控制台中输出结果的顺序很好地解释了运行时的工作过程:

  • 打印1;

  • 调用异步函数foo();

  • (在foo()中)打印2;

  • (在foo()中)await关键字暂停执行,为立即可用的值null向消息队列中添加一个任务;

  • foo()退出;

  • 打印3;

  • 同步线程的代码执行完毕;

  • JavaScript运行时从消息队列中取出任务,回复异步函数执行;

  • (在foo()中)恢复执行,await取得null值;

  • (在foo()中)打印4;

  • foo()返回。

  • 对于await后面是一个期约,问题会稍微复杂一点;为了执行异步函数,实际上会有两个任务被添加到消息队列并被添加到消息队列并被异步求值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
async function foo() { 
console.log(2);
console.log(await Promise.resolve(8));
console.log(9);
}
async function bar() {
console.log(4);
console.log(await 6);
console.log(7);
}
console.log(1);
foo();
console.log(3);
bar();
console.log(5);
// 1
// 2
// 3
// 4
// 5
// 6
// 7
// 8
// 9

执行步骤如下:
(1) 打印 1;
(2) 调用异步函数 foo();
(3)(在 foo()中)打印 2;
(4)(在 foo()中)await 关键字暂停执行,向消息队列中添加一个期约在落定之后执行的任务;
(5) 期约立即落定,把给 await 提供值的任务添加到消息队列;
(6) foo()退出;
(7) 打印 3;
(8) 调用异步函数 bar();
(9)(在 bar()中)打印 4;
(10)(在 bar()中)await 关键字暂停执行,为立即可用的值 6 向消息队列中添加一个任务;
(11) bar()退出;
(12) 打印 5;
(13) 顶级线程执行完毕;
(14) JavaScript 运行时从消息队列中取出解决 await 期约的处理程序,并将解决的值 8 提供给它;
(15) JavaScript 运行时向消息队列中添加一个恢复执行 foo()函数的任务;
(16) JavaScript 运行时从消息队列中取出恢复执行 bar()的任务及值 6;
(17)(在 bar()中)恢复执行,await 取得值 6;
(18)(在 bar()中)打印 6;
(19)(在 bar()中)打印 7;
(20) bar()返回;
(21) 异步任务完成,JavaScript 从消息队列中取出恢复执行 foo()的任务及值 8;
(22)(在 foo()中)打印 8;
(23)(在 foo()中)打印 9;
(24) foo()返回。