先记住这个答案
说它是语法糖,是因为 async/await 没有引入新的异步机制,底下跑的还是 Promise。async 函数一定返回 Promise,await p 会把函数剩下的部分挂到 p 完成之后执行,效果和写进 p.then() 的回调一样。错误处理也一一对应:try/catch 相当于 .catch(),而 finally 相当于 .finally()。它解决的是可读性问题,让异步代码能按从上到下的顺序写,而不是一层层回调。
- async 函数返回 Promise,
return值就是兑现值 await之后的代码相当于then回调,在微任务里执行try/catch对应.catch(),而finally对应.finally()- 转译到 ES2015 时,靠 Generator 加自动执行器实现
同一段逻辑的两种写法
业务里最常见的是「先拿用户,再用用户 id 拿订单」这种有依赖的两步请求。分别用 await 和 then 链写一遍,对照着看每一行去了哪里。
await 版和 then 链版JavaScriptconst getUser = () => Promise.resolve({ id: 7 })
const getOrders = (id) => Promise.resolve(['订单A', '订单B'].map((o) => o + id))
async function withAwait() {
const user = await getUser()
const orders = await getOrders(user.id) // 这一行相当于第一个 then 回调
return orders.join(',') // 相当于第二个 then 回调的返回值
}
function withThen() {
return getUser()
.then((user) => getOrders(user.id))
.then((orders) => orders.join(','))
}
withAwait()
.then((r) => console.log('await 版', r))
.then(() => withThen())
.then((r) => console.log('then 版', r))查看输出与解释
await 版 订单A7,订单B7
then 版 订单A7,订单B7两版结果完全一样。对应规则就是:每个 await 都把后面的代码切成一段,放进一个 then 回调里;await 左边的变量,就是回调的参数;函数最后 return 的值,就是整条链最终兑现的值。
await 之后的代码在什么时候执行
「放进 then 回调」这句话有一个直接的推论:await 之后的代码不会同步执行,而是等到微任务阶段。微任务是当前同步代码跑完后、浏览器渲染和下一个定时器之前,统一执行的一批回调。
await 把函数切成两半JavaScriptasync function task() {
console.log('1 await 之前,同步执行')
await null // 哪怕等的不是 Promise,也会让出执行权
console.log('3 await 之后,在微任务里执行')
}
task()
console.log('2 调用方继续往下走')查看输出与解释
1 await 之前,同步执行
2 调用方继续往下走
3 await 之后,在微任务里执行await 之前的部分和普通函数一样,调用时立刻同步执行。遇到 await 时函数暂停,把控制权交还给调用方,所以 2 先打印。改写成 then 链就是 console.log(1); return Promise.resolve(null).then(() => console.log(3)),顺序一模一样。
- 错误处理的对应关系
try { await a(); await b() } catch (e) { handle(e) } finally { done() }对应的是a().then(() => b()).catch(handle).finally(done)。catch能接住a或b任意一步的拒绝,链上的.catch()也一样,前面任何一环失败都会跳过中间的then直接落到它。
两种写法不完全等价的地方
语义上能互相改写,但写起来的难度差很多。then 链里每个回调是一个独立的作用域,第三步想同时用到第一步的 user 和第二步的 orders,要么嵌套回调,要么把两个值打包往下传。await 版本里它们都是同一个函数的局部变量,直接用就行。
- 循环和条件分支
- 「逐个请求直到拿到空页」这种循环,用
await写就是一个普通的while。用then链写,得写成递归函数,每次在then里决定要不要再调用自己。条件分支也一样,await版本就是if/else,换成then链要在回调里返回不同的 Promise。这正是 async/await 被发明出来的原因。 至于底层实现,现代引擎是原生支持 async 函数的。把代码转译到 ES2015 时,Babel 和 TypeScript 会把它变成 Generator 加一个自动执行器:Generator 每yield一个 Promise,执行器就在它的then里调用next()把结果送回去,这也从侧面说明了它和then链的等价关系。
容易答错的地方
- 以为
await会阻塞线程 await只暂停它所在的 async 函数,线程马上去执行调用方的其他代码和别的任务。上面示例里 2 先于 3 打印就是证据。真正会卡住页面的是长时间运行的同步代码,和有没有写await无关。- 以为写成
await之后就不需要处理拒绝了 await一个 rejected Promise 会在这一行抛出异常。不写try/catch,这个异常会让 async 函数返回的 Promise 变成 rejected,调用方再不处理,就成了未处理的拒绝。改成await写法后,原来链尾的.catch()要换成try/catch,不能直接删掉。- 以为两种写法的微任务时序完全相同
- 结果和先后依赖是等价的,但两条独立的链交错执行时,
await版和then版各自需要的微任务轮数不一定一样。业务代码不要依赖两条不相关的异步链谁先谁后,有顺序要求就显式地串起来。
面试官还会怎么问?
能用 Generator 手写一个简易的 async 执行器吗?
可以。写一个 run(genFn):先调用 genFn() 拿到迭代器,定义 step(value),内部调用 it.next(value)。如果 done 为 true 就用结果 resolve;否则把 yield 出来的值用 Promise.resolve 包一下,在 then 里继续调用 step,在拒绝回调里调用 it.throw(err)。整个函数返回一个 Promise,这就是 co 库的核心思路。
await 后面跟的不是 Promise 会怎样?
会先用 Promise.resolve 包一下再等。所以 await 1 拿到的就是 1,但依然会让出一次执行权,后面的代码要等到微任务阶段才执行。如果后面是带 then 方法的对象,会调用它的 then 来取值。
并发请求用 await 怎么写?
不能连着写两个 await,那是串行。要先同时发起,再用 await Promise.all([a(), b()]) 统一等待。Promise.all 本身就是 Promise 的 API,这也说明 async/await 离不开 Promise,只是换了一种写法。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。