6 个原因告诉你 Async/Await 比 Promise 好用(教程)

本文翻译自 https://hackernoon.com/6-reasons-why-javascripts-async-await-blows-promises-away-tutorial-c7ec10518dd9 , 作者 Mostafa Gaafar,翻译:Thomas Chan

你可能还不知道 Node 从 7.6 版本就已经原生支持 async/await 了。如果你还没用过的话,这儿有一堆理由和例子让你立刻对它爱不释手。

[更新]:Node 8 LTS 现在已完全支持 async/await。

Async/await 101

先给不知道本文在讲什么的同学简单介绍一下 async/await 是什么

  • Async/await 是一种新的写异步程序的语法糖,之前一直用的是回调函数和 Promise。
  • Async/await 其实建立在 Promise 之上,不能跟回调函数一起用。
  • Async/await 跟 Promise 一样是无阻塞的
  • Async/await 的代码写起来更像是同步的,这正是厉害之处。

语法

假设我们有一个 getJSON 函数返回一个 promise,promise resolve 一个 JSON 对象。我们只想调用一下这个函数并打印出 JSON 对象,然后返回 "done"

用 promise 写的话是这样:

const makeRequest = () =>
  getJSON()
    .then(data => {
      console.log(data)
      return "done"
    })

makeRequest()

而用 async/await 写的话则是这样:

const makeRequest = async () => {
  console.log(await getJSON())
  return "done"
}

makeRequest()

这有几个不同点:

  1. async 关键字在 function 前面,await 关键字只能用在使用 async 的函数内。所有 async 函数都会返回一个 promise, promise resolve 的值就是函数里 return 的值(例子里就是 “done”)
  2. 这就说明我们是不能在 async 函数外边使用 await 的。
    // 这是不能执行的
    // await makeRequest()
    
    // 这样是可以的
    makeRequest().then((result) => {
      // do something
    })
  3. await getJSON() 的意思是 console.log 将会等待 getJSON 的处理结果,然后再打印出来。

为什么 async/await 更好?

1. 简洁干净

我们将会少些很多代码!在上例里我们只写了三行代码。我们不需要再写 .then,创建一个匿名函数处理 response,或者把 data 赋值给一个可能不需要的变量。同时我们也避免了代码嵌套。这个优点在下边更多的例子显而易见。

2. 错误处理

Async/await 终于能让我们用 try/catch 在一个层级里处理 同步异步 的错误了。在下面的 promise 例子里,如果 JSON.parse 报错了,try/catch 是抓不住的,因为 JSON.parse 在 promise 里。我们需要在 promise 后加上 .catch 再重复一遍我们的错误处理逻辑,可能才会在真报错的时候帮到我们知道具体错误是什么。

const makeRequest = () => {
  try {
    getJSON()
      .then(result => {
        // JSON.parse 可能会 parse 失败
        const data = JSON.parse(result)
        console.log(data)
      })
      // 取消 catch 的注释来处理异步错误
      // .catch((err) => {
      //   console.log(err)
      // })
  } catch (err) {
    console.log(err)
  }
}

让我们再来看看相同的代码用 async/await 的实现。catch 是会抓到 parse 失败的错误的。

const makeRequest = async () => {
  try {
    // JSON.parse 可能会 parse 失败
    const data = JSON.parse(await getJSON())
    console.log(data)
  } catch (err) {
    console.log(err)
  }
}

3. 条件判断

像下边这个例子里,先获取数据然后判断是直接返回该数据还是需要再获取别的数据。

const makeRequest = () => {
  return getJSON()
    .then(data => {
      if (data.needsAnotherRequest) {
        return makeAnotherRequest(data)
          .then(moreData => {
            console.log(moreData)
            return moreData
          })
      } else {
        console.log(data)
        return data
      }
    })
}

这代码看着都头疼,一不小心就迷失在多层嵌套(6 层)、数据准备、多处 return。

而用 async/await 重写后就很简单易懂了。

const makeRequest = async () => {
  const data = await getJSON()
  if (data.needsAnotherRequest) {
    const moreData = await makeAnotherRequest(data);
    console.log(moreData)
    return moreData
  } else {
    console.log(data)
    return data
  }
}

4. 中间值

你可能会遇到这种情况,你调用了 promise1,然后用返回值去调 promise2, 然后再用 promise1promise2 的返回值去调 promise3,代码像这样

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return promise2(value1)
        .then(value2 => {
          // do something
          return promise3(value1, value2)
        })
    })
}

如果 promise3 不需要 value1 还能少嵌套一层。如果你实在受不了多层嵌套,还能像下边这样用 Promise.all 包装一下 1 和 2

const makeRequest = () => {
  return promise1()
    .then(value1 => {
      // do something
      return Promise.all([value1, promise2(value1)])
    })
    .then(([value1, value2]) => {
      // do something
      return promise3(value1, value2)
    })
}

这样的写法牺牲了一些可读性。value1value2 除了避免多层嵌套好像没什么其他理由应该在一个数组里。

相同的逻辑用 async/await 实现简直不要太简单和直观,是不是让你想起了曾经为了让 promise 不那么难看所花的时间。

const makeRequest = async () => {
  const value1 = await promise1()
  const value2 = await promise2(value1)
  return promise3(value1, value2)
}

5. 错误堆栈

想象我们有一段代码,用链的形式调用了一堆 promise,然后在某个地方抛出一个错误。

const makeRequest = () => {
  return callAPromise()
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => callAPromise())
    .then(() => {
      throw new Error("oops");
    })
}

makeRequest()
  .catch(err => {
    console.log(err);
    // output
    // Error: oops at callAPromise.then.then.then.then.then (index.js:8:13)
  })

promise 链返回的错误堆栈信息完全看不出来是哪儿报的错。更糟的是堆栈信息还具有误导性,信息里所包含的唯一一个函数名字 callAPromise 完全跟这个错误没关系(不过文件名和代码行数还是有用的)。

然而 async/await 给的错误堆栈是直接指向报错的函数的

const makeRequest = async () => {
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  await callAPromise()
  throw new Error("oops");
}

makeRequest()
  .catch(err => {
    console.log(err);
    // 输出
    // Error: oops at makeRequest (index.js:7:9)
  })

当然这在你本地开发模式下没多大帮助,但是在线上 debug 的时候还是非常有用的。像这种情况下,如果能知道错误是来自 makeRequest 肯定要好过来自 then.then.then…..

6. Debugging

最后一点,async/await 的一个杀手锏就是非常容易 debug。Debug promises 有两个痛点

  1. 不能在箭头函数里设置断点。
  2. 如果你在 .then 里设置断点,像平时一样一步一步看函数的执行,debugger 是不会跟到 .then 里去的,因为它只能按步执行同步的代码。

而用 async/await 你不需要箭头函数,而且你能像 debug 同步的代码一样来 debug await。

总结

Async/await 是近几年 JavaScript 新增的几个革命性 features 之一,它让人意识到 promises 的语法是多么的乱。

注意

  • 很明显异步代码写起来行数会变少,你可能需要适应一下新的语法,不过熟悉 C# 的人应该很熟悉这个语法了。
  • 是的,Note 7 不是 LTS 版本,不过 node 8 下个月就发布了,而且迁移代码到新版本也没有什么影响。[更新]:Node 8 LTS 发了。