likes
comments
collection
share

为什么说 js 中 async/await 其实是语法糖

作者站长头像
站长
· 阅读数 10

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 方法返回的对象的 doneture。其实不需要我们自己手写 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() ,无需再去执行什么 processGeneratorco,就可以打印得到结果:

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 这种执行器的语法糖。

为什么说 js 中 async/await 其实是语法糖 为什么说 js 中 async/await 其实是语法糖