异步编程03:async-await

阅读 2.3k
标签: JavaScript

async的本质

async是一个关键字,用来声明一个异步函数(AsyncFunction),并且一定会返回一个Promise对象

async function asyncSum(n1, n2) {
  return n1 + n2
}

上面代码等价于

function asyncSum(n1, n2) {
  return Promise.resolve(n1 + n1)
}

其实,async可以看成是一种语法糖,内部引擎执行过程如下:

let asyncFunc = async function () {}
let AsyncFunction = Object.getPrototypeOf(asyncFunc).constructor
let asyncSum = new AsyncFunction('n1', 'n2', 'return n1 + n2')

函数体的执行过程

既然asyncSum是一个异步函数,那么其执行过程是怎样的呢?

async函数的函数体可以看作是由0个或者多个await表达式组成的。从第一行代码直到第一个await表达式(如果有的话)都是同步运行的。

这样的话,一个不含await表达式的async函数是会同步运行的。然而,如果函数体内有一个await表达式,async函数就一定会异步执行。

返回值

async 函数一定会返回一个Promise对象,其返回值的规则如下:

  • 如果async 函数执行成功,返回的Promise对象的状态为fulfilled,值为return中的值。
  • 如果执行失败,那么返回的Promise对象的状态为rejected,值为相关的错误信息。

为什么需要async

引入async func,其实借鉴了迭代器(iterator)中的处理方式。

想象一下,对多个前后依赖的异步任务,如果可以像迭代数组一样进行迭代处理,那么异步任务的代码,会更加的清晰易懂。

Promise.then链在处理多个异步任务(各个异步任务是依赖关系,就是说,后面一个任务依赖前一个任务的执行结果)时,需要在每个then中设置需要返回的Promise对象,稍显麻烦。

比如,常见的先读取再写入的例子,用Promise.then链式调用:

readFileTask
  .then(readData => {
    console.log(`read finished: ${readData}`)
    return writeFile('example_copy.json', readData) // 继续写入
  })
  .then(msg => {
    console.log(`write finished: ${msg}`)
  })
  .catch(err => console.log(err))

如果用async-await:

async function task() {
  const r1 = await readFile('example.json')
  const r2 = await writeFile('example_copy.json', r1)
}

task()

虽然是异步代码,但是看上去却是同步的方式。也就是说,async-await能够让你以同步方式写异步代码。

不是取代Promise

可能有些人认为,有了async-await,Promise就没有存在的必要了。这是错误的看法!

async-await,只是在处理多个异步任务(且各个异步任务是依赖关系,就是说,后面一个任务依赖前一个任务的执行结果时,会让代码更加清晰,它不是为了取代Promise,也无法取代Promise,二者有不同的使命。Promise的出现是为了解决回调的问题,而async-await,从yield具有的代码暂停执行功能中找到了灵感,提供了类似的异步代码暂停执行的能力,用同步的方式去写异步代码。

当然,async结合await才具有真正的威力,就像generator一样,其存在的价值是为了服务iterator

await

await就容易理解了,光看字面意思大概也能猜出来,它其实就是等待一个异步任务的执行结果。它只能在async function中使用。

语法:

[返回值] = await 表达式

表达式:一个Promise对象或者任何要等待的值。

返回值:Promise对象的处理结果。如果等待的不是Promise对象,则返回该值本身。

await表达式会暂停当前async function的执行,等待Promise处理完成。

来看一个例子:

async function task() {
  console.log('task start..')
  const result = await Promise.resolve(100)
  console.log(result)
  console.log('task finished..')
}
console.log(1)
const r = task()
console.log(2)

上面代码中,会先打印1,然后执行task函数体,接着打印出“task start..”,这里遇到await表达式,会等待异步任务执行,所以,会打印出2,打印出2后,所有同步任务已经执行完毕,继续执行异步代码,当await执行结束后,打印出结果100,最后打印出“task finished..”,所以,结果为:

1
task start..
2
100
task finished..

返回值

我们需要重点关注返回值,一般有如下几种情况:

  • 若Promise正常处理(fulfilled),其回调的resolve函数的参数作为await表达式的值,继续执行async function。
  • 若Promise发生异常(rejected),await表达式会把Promise的异常原因抛出。
  • 如果await后面表达式的值不是一个Promise,则返回该值本身。

最后,再重复一下:

  • 当执行到await表达式时,代码会暂停当前async function的执行,等待Promise处理完成。
  • await最终拿到的是结果,即Promise.resolve中的值(如果成功执行),而不是一个Promise对象。

小结

到这里为止,已经将异步编程说得差不多了。不过,还有一个比较重要的点没有提到,那就是异常处理,异常处理非常重要,尤其是在Node.js中,因为如果异常处理不当,很可能整个服务器就崩溃了。关于如何正确在异步代码中进行异常处理,请见本系列的最后一篇《异步编程04:异常处理》。

最后编辑于: 2022-06-28

评论(0条)

(必填)
复制成功