11.3 异步函数
任何要访问期约产生值的代码,
都需要以处理程序的形式来接受此值:
1 | function handler(x) { console.log(x); } |
这种书写方式相当笨重,
ES8新增了 async/await 关键字,
让以同步方式写的代码能够异步执行。
11.3.1 异步函数
async语法
适用范围:
- 函数声明
- 函数表达式
- 箭头函数
- 方法
1 | async function foo() { } //函数声明 |
async功能
让函数具有异步特征,但总体上代码仍然是同步求值。
1 | async function foo() { |
async返回值
始终返回 期约对象。
返回值会被 Promise.resolve() 包装为一个期约对象。
1 | async function foo() { |
async返回值的静默处理
async函数的返回值实际期待的是:
一个实现thenable接口的对象
对于是否实现了这一条件,async会对返回值采取不同的静默处理方式:
- 实现thenable接口 在调用返回值的then方法时,可以提供onResolve/onRejected处理程序以供解包
- 未实现thenable接口 返回值被当做已经已解决的期约
返回一个原始值
1 | async function foo() { |
返回一个没有实现thenable接口的对象
1 | async function bar() { |
返回一个实现了thenable接口的对象
1 | async function baz() { |
返回一个期约
1 | async function qux() { |
异步函数中抛出错误
会返回拒绝期约
1 | async function foo() { |
拒绝期约的错误
不会被异步函数捕获
1 | async function foo() { |
await功能
await关键字会 暂停执行异步函数后面的代码 。
第一步
尝试解包对象的值。
第二步
将值传给表达式。
第三步
异步恢复异步函数的执行。
1 | async function foo() { |
await用法
用法相当于 一元操作符:
异步打印”foo“
1 | async function foo() { |
异步打印”bar“
1 | async function bar() { |
1000ms后异步打印”baz“
1 | async function baz() { |
await的静默处理
await关键字实际期待的是:
一个实现了thenable接口的对象
对于是否实现thenable接口,await对其也有不同的静默处理方式:
- 实现了thenable接口: await会对其进行解包
- 未实现thenable接口: 默认包装为已解决的期约
await一个原始值
1 | async function foo() { |
await一个没有实现thenable接口的对象
1 | async function bar() { |
await一个实现了thenable接口的对象
1 | async function baz() { |
await一个期约
1 | async function qux() { |
await抛出错误的同步操作
会返回拒绝的期约。
1 | async function foo() { |
await拒绝的期约
会 释放(unwrap)错误值。
即直接将拒绝期约作为返回值return。
1 | async function foo() { |
await的限制
await关键字只能 直接出现在异步函数的定义中 ,
它的异步特质不会扩展到嵌套函数中。
不能在顶级上下文中使用,但是可以定义并立即调用:
定义并立即调用的异步函数
1 | (async function () { |
不允许:await出现在箭头函数中
1 | function foo() { |
不允许:await出现在同步函数声明中
1 | function bar() { |
不允许:await出现在同步函数表达式中
1 | function baz() { |
不允许:IIFE使用同步函数表达式或箭头函数
1 | function qux() { |
11.3.2 停止和恢复执行
异步函数中真正起作用的实际是 await,
async更像是一个 标识符,
异步函数如果不包含await关键字,执行起来实际和普通函数没有区别:
1 | async function foo() { |
第一步
记录暂停执行的位置。
第二步
等到对象值可用时,会向消息队列推送一个任务。
第三步
该任务恢复异步函数的执行(回到之前记录的暂停位置)
1 | async function foo() { |
step 1
打印1
step 2
进入foo(),打印2
step 3
遇到await关键字,记录暂停位置,向消息队列中添加一个任务
step 4
暂时退出foo(),打印3
step 5
同步线程代码执行完毕,从消息队列中取出任务,返回之前记录位置恢复异步函数执行
stup 6
打印4,退出foo()
1 | async function foo() { |
step 1
打印1
step 2
进入foo,打印2
step 3
遇到await关键字暂停执行,记录暂定位置,向消息队列中添加一个期约在落定之后执行的任务
step 4
期约立即落定,把给await提供值的任务添加到消息队列,暂时从foo中退出
step 5
打印3
step 6
进入bar,打印4
step 7
遇到await关键字暂停执行,向消息队列添加一个任务,暂时退出bar
step 8
打印5,线程执行完毕
step 9
退回到foo暂停位置,打印8,打印9,退出foo()
step 10
退回到bar暂停位置,打印6,打印7,退出bar()
11.3.3 异步函数策略
1. 实现sleep()
可以实现类似Java中的 Tread.sleep() 函数,
在程序中加入非阻塞的暂停:
1 | async function sleep(delay) { |
2. 利用平行执行
1 | async function randomDelay(id) { |
1 | async function randomDelay(id) { |
3. 串行执行期约
1 | async function addTwo(x) { return x + 2; } |
4. 栈追踪与内存管理
比较 期约 与 内存管理 在拒绝期约时的栈追踪信息:
期约
1 | function fooPromiseExecutor(resolve, reject) { |
栈追踪信息
Uncaught (in promise) bar
setTimeout (async)
fooPromiseExecutor
foo
JavaScript引擎会在创建期约时 尽可能保留完整的调用栈 ,
这意味着栈追踪信息会占用内存,带来一些计算和存储成本。
异步函数
1 | function fooPromiseExecutor(resolve, reject) { |
栈追踪信息
Uncaught (in promise) bar
foo
await in foo (async)
栈追踪信息可以准确地反应当前的调用栈。
JavaScript在嵌套函数中存储指向包含函数的指针,
指针存储在内存中,可以用于在出错时生成追踪信息。
因此能够降低内存的额外消耗。
栈追踪信息应该相当直接地表现JavaScript引擎当前栈内存中函数调用之间的嵌套关系。