理解 async/await

4,523 阅读6分钟

什么是async?


现在面对日常工作时,总避免不了面对异步操作带来的一些麻烦。在时代演变的过程中,处理异步的方法有许多种:回调函数、Promise 链式语法、Generator 函数到现在比较流行的 async 函数。那什么是 async 呢?

async 函数是 Generator 函数的语法糖。使用 async 关键字代替 Generator 函数的星号 *await 关键字代替 yield。相较于Generator函数,async函数改进了以下四点:

  • 内置执行器 Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。
  • 更好的语义 asyncawait,比起 *yield,语义更清楚。async 表示函数里有异步操作,await 表示紧跟在后面的表达式需要等待结果。
  • 更广的适用性 co 模块约定,yield 命令后面只能是Thunk 函数或Promise 对象,而async 函数的await 命令后面,可以是Promise对象和原始类型的值。
  • 返回值是 Promise async 函数的返回值是Promise对象,这比Generator 函数的返回值是 Iterator对象方便多了。你可以用 then 方法指定下一步的操作。

async 用法


关于async的用法,先看一个简单的小例子:

function getProvinces () {
    return new Promise(resolve => {
        setTimeout(resolve, 1000)
    })
}
async function asyncFn () {
    await getProvinces()
    console.log('hello async')
}

上面代码先定义了一个获取省份数据的getProvinces函数,其中用setTimeout模拟数据请求的异步操作。当我们在asyncFn 函数前面使用关键字 async 就表明该函数内存在异步操作。当遇到 await 关键字时,会等待异步操作完成后再接着执行接下去的代码。所以代码的执行结果为等待1000毫秒之后才会在控制台中打印出 'hello async'。

了解了 async 的基本用法,接下来理解一下 async 的运作:

async function asyncFn1 () {
    return 'hello async'
}
asyncFn1().then(res => {
    console.log(res)
})
// 'hello async'

async function asyncFn2 () {
    throw new Error('error')
}
asyncFn2().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})
// 'error'

async 会返回一个Promise对象,当没发生错误时 return 的值会成为 then 方法回调函数的参数。而当抛出错误时,会导致Promise对象变为 reject 状态,抛出的错误也会成为 catch 方法回调函数的参数。

async function asyncFn3 () {
    return await Promise.resolve('hello async')
}
asyncFn3().then(res => console.log(res))
// 'hello async'

async function asyncFn4 () {
    return await 123
}
asyncFn3().then(res => console.log(res))
// 123

await(async wait)关键字后面如果是一个Promise对象,则会返回该Promise的结果。如果不是,也会当成立即执行resolve,将值返回。

async 函数当中存在多个 await 的函数时,我们不得不考虑某个Promise状态变为 reject 的情况,因为只要内部有函数状态改变为 reject 时,接下去的函数将不再执行,async 函数的状态也将变更为 reject

async function asyncFn5 () {
    await Promise.reject('error')
    return await Promise.resolve('hello async') // 不会执行
}
asyncFn5().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

为了能够正确的执行代码,应该对 await 进行错误处理,基本的错误处理方式有两类:

async function asyncFn6 () {
    try {
        await Promise.reject('error')
    } catch (err) {
        console.log(err)
    }
    return await Promise.resolve('hello async')
}
// 将可能发生错误的函数使用try...catch进行处理

async function asyncFn7 () {
    await Promise.reject('error').catch(err => console.log(err))
    return await Promise.resolve('hello async')
}
// 将可能变为reject状态的Promise对象后面跟上一个catch方法,以处理之前发生的错误

理解了 async/await 的基本用法,接下来用一个工作中经常会遇到的情景作为例子,感受一下 async/await 的魔力。

假设我们现在要获取一个地级市拥有多少个辖区,我们现在得先调用获取当地省份的接口,从中拿到省份id才能够调用获取地级市的接口,拿到对应地级市的id才能获取最终的结果。

function getProvinces () {
    ...
    return new Promise(resolve => {
        resolve(provinceId)
    }
}
function getCitys (provinceId) {
    ...
    return new Promise(resolve => {
        resolve(cityId)
    }
}
function getArea (cityId) {
    ...
    return new Promise(resolve => {
        resolve(areaData)
    }
}

如果用 Promise 实现是这样:

getProvinces().then(provinceId => getCitys(provinceId)).then(cityId => getArea(cityId)

再来看看用 async/await 实现方式:

async getData () {
    const provinceId = await getProvinces()
    const cityId = await getCitys(provinceId)
    return await getArea(cityId)
}

getData()

虽然两种方法都能够达到我们最终的目的,但是在依赖关系更加复杂的情况下,使用 Promise 的方式会使得链式非常的长,并且相比使用 async/await 代码阅读性会更低。

async运行过程

在工作中 async 的应用情况更加多种,因为其看似同步的处理异步操作,解决了不断回调的问题,增加了代码的可阅读性。 async 虽然看似同步操作,但是它式非阻塞的,接下来将 asyncPromisesetTimeout 结合,用一个小例子加深对 async 的理解:

async function asyncFn1 () {
    console.log('asyncFn1 start')
    await asyncFn2()
    console.log('async1 end')
}

async function asyncFn2 () {
    console.log('asyncFn2')
}

console.log('script start')

setTimeout(function () {
    console.log('setTimeout')
}, 0)

asyncFn1()

new Promise((resolve) => {
    console.log('Promise')
    resolve()
}).then(() => {
    console.log('Promise.then')
})
console.log('script end')

上面的代码,运行过程中会打印出8条语句,请大家先花一些时间思考一下执行顺序。

最终在控制台中的打印结果为:

script start
asyncFn1 start
asyncFn2
Promise
script end
Promise.then
async1 end
setTimeout

应该有许多人的答案都是正确的,假如你的答案与正确答案有些许偏差,也没关系,通过这道题你能更深入的理解异步执行的问题,这段代码的执行顺序其实是这样的:

  1. 定义异步的asyncFn1函数
  2. 定义异步的asyncFn2函数
  3. 执行console.log('script start')语句 * 1
  4. 定义一个定时器在0ms后输出(setTimeout会被加入到macrotasks队列中,所以执行优先级比被加入microtasks队列的低)
  5. 执行asyncFn1函数 :

(1)执行console.log('asyncFn1 start')语句 * 2

(2)遇到await,执行asyncFn2函数 * 3(此时让出线程,跳出asyncFn1函数,继续执行同步栈的任务)

  1. 执行Promise语句

(1)执行console.log('Promise')语句 * 4

(2)resolve(),返回一个Promise对象,将这个Promise对象加入到microtasks队列中

  1. 执行console.log('script end')语句 * 5
  2. 同步栈执行完毕
  3. 回到asyncFn1函数体中,将asyncFn2函数返回的Promise对象加入到microtasks队列中
  4. 取出microtasks队列中的任务,打印console.log('Promise.then') * 6
  5. 接着执行asyncFn1函数体中console.log('asyncFn1 end')语句 * 7
  6. 最后执行macrotasks队列中的任务,执行console.log('setTimeout') * 8

以上是我对async/await知识的一些拙见,写下这篇文章单纯为了巩固自身的知识,希望也能对读者有一点点帮助。

本文总结参考自:阮一峰的ESMAScript6入门以及前端er,你真的会用async吗?