async & await 异步函数
难度:⭐️⭐️⭐️
💌
async/await
是 ES8 规范新增的。这个特性从行为和语法上都增强了 JavaScript,让以同步方式写的代码能够异步执行。
下面来看一个最简单的例子,这个期约(promise
)在超时之后会解决为一个值:
let p = new Promise((resolve, reject) => setTimeout(resolve, 1000, 3));
如果程序中的其他代码要在这个值(3)可用时访问它,则需要写一个解决处理程序:
p.then((x) => console.log(x)); // 3
这其实是很不方便的,因为其他代码都必须塞到期约处理程序中。ES8 为此提供了 async/await
关键字。
异步函数
async
await
ES8 的 async/await
旨在解决利用异步结构组织代码的问题,为其增加了两个新关键字:async
和 await
。
async
🧱 async
关键字用于声明异步函数。使用 async
关键字可以让函数具有异步特征,但总体上其代码仍然是同步求值的❗️。而在参数或闭包方面,异步函数仍然具有普通 JavaScript 函数的正常行为:
async function foo() {
console.log(1);
}
foo();
console.log(2);
// 1
// 2
🧱 异步函数如果使用 return
关键字返回了值(如果没有 return
则会返回 undefined
),这个值会被 Promise.resolve()
包装成一个期约对象。异步函数始终返回期约对象:
async function foo() {
console.log(1);
return 3;
}
// 给返回的期约添加一个解决处理程序
foo().then(console.log);
console.log(2);
// 1
// 2
// 3
🧱 异步函数的返回值期待(但实际上并不要求)一个实现 thenable
接口的对象,但常规的值也可以:
// 返回一个原始值
async function foo() {
return 'foo';
}
foo().then(console.log);
// foo
// 返回一个实现了 thenable 接口的非期约对象
async function baz() {
const thenable = {
then(callback) { callback('baz'); }
};
return thenable;
}
baz().then(console.log);
// baz
- 如果返回的是实现
thenable
接口的对象,则这个对象可以由提供给then()
的处理程序 “解包”- 如果不是,则返回值就被当作已经解决的期约
⚠ 不过,拒绝期约的错误不会被异步函数捕获:
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
await
因为异步函数主要针对不会马上完成的任务,所以自然需要一种暂停和恢复执行的能力。使用 await
关键字可以暂停异步函数代码的执行,等待期约解决。
// 1000 毫秒后异步打印"baz"
async function baz() {
await new Promise((resolve, reject) => setTimeout(resolve, 1000));
console.log('baz');
}
baz();
// baz(1000 毫秒后)
❗️ 注意:await
关键字会暂停执行异步函数后面的代码,让出 JavaScript 运行时的执行线程(这个行为与生成器函数中的 yield
关键字是一样的)。await
关键字同样是尝试“解包”对象的值,然后将这个值传给表达式,再异步恢复异步函数的执行。
await
关键字期待(但实际上并不要求)一个实现 thenable
接口的对象,但常规的值也可以。这一点跟异步函数的返回值一致:
// 等待一个原始值
async function foo() {
console.log(await 'foo');
}
foo();
// foo
// 等待一个实现了 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
如我们在异步函数里演示的,单独的 Promise.reject()
不会被异步函数捕获,而会抛出未捕获错误。But❗️ However❗️ 对拒绝的期约使用 await
则会释放(unwrap)错误值(将拒绝期约返回):
async function foo() {
console.log(1);
await Promise.reject(3);
console.log(4); // 这行代码不会执行
}
// 给返回的期约添加一个拒绝处理程序
foo().catch(console.log);
console.log(2);
// 1
// 2
// 3
await 的限制
await
关键字必须在异步函数中使用,不能在顶级上下文如<script>
标签或模块中使用。
停止和恢复执行
TC39 对
await
后面是期约的情况如何处理做过一次修改。修改前:Promise.resolve()
会生成两个异步任务;修改后:Promise.resolve()
只会生成一个异步任务。接下来所示代码的结果都是现在(修改后)的实际情况。
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
// foo
// bar
async/await
中真正起作用的是 await
。async
关键字,无论从哪方面来看,都不过是一个标识符(好轻蔑的语气 😂)。毕竟,异步函数如果不包含 await
关键字,其执行基本上跟普通函数没有什么区别。
要完全理解 await
关键字,必须知道它并非只是等待一个值可用那么简单。JavaScript 运行时在碰到 await
关键字时,会记录在哪里暂停执行。等到 await
右边的值可用了,JavaScript 运行时会向消息队列中推送一个任务,这个任务会恢复异步函数的执行。
因此,即使 await
后面跟着一个立即可用的值,函数的其余部分也会被异步求值:
async function foo() {
console.log(2);
await null; // null 是一个立即可用的值,但 await 也会暂停执行
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
后面是一个期约,也是一样的执行顺序,没有区别:
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
// 8
// 9
// 6
// 7
这里书中应该写的是修改前的情况,即:
await
后面是一个期约的时候,为了执行异步函数,会有两个任务被添加到消息队列并被异步求值。现在(修改后)更好理解 🤗。
✨ 总之,通过期约和 async/await
,不仅可以实现之前难以实现或不可能实现的任务,而且也能写出更清晰、简洁,并且容易理解、调试的代码。