为什么说 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