为什么说 js 中 async/await 其实是语法糖
ES8(ES2017)推出的 async 函数,结合 await 关键字,号称是回调地狱的终极解决方案。但究其本质,其实是 Generator 的一种语法糖。下面我们就一步步进行说明。
假设现在有个如下返回 promise 的异步请求 request:
// 例 1
function request(num) {
return new Promise(resolve => {
setTimeout(() => {
resolve(num + 1)
}, 1000)
})
}
给 request() 传入一个数 num,1s 后再将 num 加 1 传给 resolve(),然后可以在相应的 then 方法中得到 num+1 的值。如果我们现在有个需求:一开始给 request 传入的数字 0,想得到数字 3,我们应该怎么做呢?下面介绍 4 种不同的写法:
1. 回调地狱
可以像例 1.1 这样,依次在 then 方法中发起请求,并将上一次请求的结果传给 request:
// 例 1.1
request(0).then(res => {
request(res).then(res => {
request(res).then(res => console.log(res))
})
})
如此,3s 后就能打印得到数字 3 。但这种写法,形成了所谓的回调地狱,降低了代码的可读性和维护性。
2. 链式调用
于是,我们可以将例 1.1 的代码改改,变成下面这样:
// 例 1.2
request(0)
.then(res => request(res))
.then(res => request(res))
.then(res => console.log(res))
在 then 的回调函数中,直接将 request() 返回,由于 request() 的执行返回的是一个 promise,那么这个 then 方法本身返回的 promise 的结果就会由 request() 返回的这个 promise 的结果决定,于是我们就可以继续以链式调用 then 方法的形式来发起请求,从而避免了回调地狱的产生。这种写法在像例 1.2 这样每个 then 方发的回调中处理逻辑简单时似乎没什么问题,但如果处理逻辑比较复杂,代码量大的时候可读性依旧不佳。
3. 生成器函数结合 promise
我们再将例 1.2 的代码进行下修改,写个生成器函数并与 promise 进行结合:
// 例 1.3
function* getNum() {
const res1 = yield request(0)
const res2 = yield request(res1)
const res3 = yield request(res2)
console.log(res3)
}
const generator = getNum()
generator.next().value.then(res => {
generator.next(res).value.then(res => {
generator.next(res).value.then(res => {
generator.next(res)
})
})
})
getNum是个生成器函数,第 9 行被调用时,其内部的代码并不会执行,而是返回了生成器 generator。当我们在第 10 行第 1 次调用生成器的 next() 方法,第 3 行代码才会执行,执行完后暂停。
yield 后面的 request(0) 会作为 generator.next() 的返回对象的 value 的值,也就是说 generator.next().value 得到的是一个 promise,当它的 then 方法的回调执行时,说明第 1 次请求已经得到结果。于是再次执行了第 11 行的 generator.next(),并将结果传入,由第 3 行的 res1 接收。这样第 4 行执行第 2 次请求时,就可以将前一次请求得到的结果作为参数传给 request()了。以此类推,最终第 6 行将在代码执行 3s 后打印得到结果 3。
现在,getNum 里的代码看起来就舒服多了。但是第 10 ~ 16 行多出了个回调地狱怎么解决呢?我们可以写个函数对其封装,之后只要把生成器函数传给该函数执行 processGenerator(getNum),即可自动帮我们完成例 1. 3 中第 10 ~ 16 行代码所做的事:
// 例 1.3.1
function processGenerator(generator) {
const gr = generator()
function recursive(res) {
const result = gr.next(res)
if (result.done) {
return result
}
result.value.then(res => {
recursive(res)
})
}
recursive()
}
因为我们不知道到底要调用几次 next 方法,所以写了个递归函数 recursive,结束递归的条件就是 next 方法返回的对象的 done 为 ture。其实不需要我们自己手写 processGenerator,tj 大神已经写好了一个叫做 co 的库可以直接拿来用,如果是在 node 环境下执行代码,可以直接npm i co,然后 require 引入下再把生成器函数传入即可:
// 例 1.3.2
const co = require('co')
co(getNum)
4. async/await
现在再次看一眼例 1.3 的生成器函数 getNum(),我们只需要将 * 去掉,在 function 前写个 async,将 yield 换成 await,之后直接运行 getNum() ,无需再去执行什么 processGenerator 或 co,就可以打印得到结果:
async function getNum() {
const res1 = await request(0)
const res2 = await request(res1)
const res3 = await request(res2)
console.log(res3)
}
所以,async/await 的本质,可以看成是上文第 3 种方法,生成器函数配合类似于 co 这种执行器的语法糖。

转载自:https://juejin.cn/post/7245297060612833339