JS异步编程(5)-async/await

849 阅读5分钟

这是我参与更文挑战的第5天,活动详情查看:更文挑战

async/await 是什么

了解和使用 async 之前,需要提前了解以下部分:

  • Event loop
  • Promise
  • Generator

async/awaitES7 专门为异步编程设计的语法,本质上是 Generator 的语法糖
在继承了 Generator 可分段执行程序的能力之外,弥补了 Generator 本身的不足

// async/await 之前
// 主要还是使用 Promise 链式调用的方式,形式上还是“链式调用” + “回调函数”
function task() {
    task1()
    .then(() => task2)
    .then(() => task3)
}

// async/await 之后
// 真正同时做到了“异步任务按序执行”的“顺序执行写法”
async function task() {
    await task1()
    await task2()
    ...
}

基本执行流程:

  1. async 函数 task 执行到第一个 await 异步任务 task1,会将执行线程让给 task1
  2. 直到 task1 正常返回,才会重新获得执行线程,继续执行第二个 await 异步任务 task2
  3. 依次类推

重点主要分为两个部分:async 关键字和 await 关键字

async 关键字

async 类似 Generator 中的 *,用于将函数定义为 async 函数
成为 AsyncFunction 构造函数的实例
AsyncFunction 参考

async 函数具备几个特点

  1. 语义清晰明确
  2. 返回 Promise 对象
  3. 自带执行器,开箱即用
  4. 可以被 try catch 捕获代码错误

语义清晰明确

相比于 *yieldasync 异步,await 等待,清晰明确,没有歧义

返回 Promise 对象

返回值是 Promise 类型的对象,方便使用 Promise API
便于各类异步场景的运用

  • 如果 async 函数返回的不是一个 Promise 对象,会使用 Promise.resolve() 进行处理返回
  • 如果 async 函数返回一个 Promise 对象,以这个对象为准
  • 如果 async 函数报错或者 reject,会返回一个 rejectPromise 对象

自带执行器,开箱即用

不同于 Generator 函数需要使用 co 之类的函数库封装执行器
async 开箱即用,不需要额外的封装

可以被 try catch 捕获代码错误

之前的异步编程方案,例如 Promise 在异步任务中的代码错误,无法被 try catch 捕获
但是在 async 函数中,代码执行错误可以被 try catch 捕获了

// Promise 
function promiseFun() {
    try {
      errFun()
    } catch(err) {
      console.log('err', err) // 不会执行
    }
}

// async
async function asyncFun() {
    try {
      await errFun()
    } catch(err) {
      console.log('err', err) // 会执行
    }
}

// 模拟异步代码错误
async function errFun() {
  return Promise.resolve().then(() => {
    '123'.filter(item => item.name) // 这里代码执行错误
  })
}

await 关键字

await 类似 Generator 中的 await,用于移交执行线程给其他函数

await 只能在 async 函数中使用

await 只能在 async 函数中使用,不能在其他类型函数中使用
否则会报错!

await 顺序执行会被 rejected 的 Promise 阻断

如下所示: await 对应的表达式或者函数返回值,如果是一个 rejected 状态 的 Promise
async 函数会被中断执行

async function test(){
    await Promise.resolve(1);
    await Promise.reject(2);
    await Promise.resolve(3); // 程序无法执行
    return 'done';
}

因此,async/await 的一个重头戏就是错误处理

async/await 在使用中的问题

async/await 在使用中的问题基本有两个

  1. 错误处理
  2. 在循环迭代中的使用

async 错误处理

async 函数错误的基本处理方式有 try catchPromise catch 两种方式

// try catch
async function fun1() {
  try {
      await somethingThatReturnsAPromise()
  } catch(err) {
      console.log(err)
  }
}

// Promise catch
async function fun2() {
  await somethingThatReturnsAPromise().catch((err) => {
    console.log(err);
  });
}

基于以上的两种方式,延伸出两种对应的优化方案

循环迭代中使用 async

我们平时常用的循环迭代方法,在与 async/await 结合使用时,会出现一些意料之外的情况

  • work:按时间间隔执行打印
  • no work:同时打印

下面是各个方案的结果:

  • for 循环:work
  • for in:work
  • for of:work
  • while:work
  • forEach:no work
  • map:no work
  • filter:no work
  • reduce:no work
// 工具函数
// 期望通过循环 list 执行 setTimeoutFun
// 实现每隔 1000ms 打印日志
const list = [1000, 1000, 1000]
const setTimeoutFun = (num) => {
    return new Promise((resolve) => {
        setTimeout(() => {
            console.log(num, new Date())
            resolve(num)
        }, num)
    })
}

// for 循环
async function forFun() {
    for (let i = 0; i < list.length; i++) {
        await setTimeoutFun(list[i])
    }
}

// for in
async function forInFun() {
    for (const i in list) {
        await setTimeoutFun(list[i])
    }
}

// for of
async function forOfFun() {
    for (const num of list) {
        await setTimeoutFun(num)
    }
}

// while
async function whileFun() {
    let i = 0
    while(i <= list.length - 1) {
        await setTimeoutFun(list[i])
        i += 1
    }
}

// forEach
function forEachFun() {
    list.forEach(async (num) => {
        await setTimeoutFun(num)
    })
}

// map
function mapFun() {
    list.map(async (num) => {
        await setTimeoutFun(num)
    })
}

// filter
function filterFun() {
    list.filter(async (num) => {
        await setTimeoutFun(num)
    })
}

// reduce
function reduceFun() {
    list.reduce(async (pre, next, index) => {
        return await setTimeoutFun(next)
    }, Promise.resolve())
}

forEachmapfilter 通过查看 polyfill 源码,可知
内部是通过 while 循环 的方式调用 callback,这个循环没有使用 async/await,不会等待异步任务执行完成
因此 forEachmapfilter 会出现近似于同时执行多个异步任务的情况

reduce 比较特殊
通过查看 reduce polyfill 源码,可知 reduce 也是通过 while 循环的方式调用 callback
所以原理上和 forEachmapfilter 一样,也是同时执行多个异步任务

但是可以通过一些优化来实现 reduce + async/await

  1. async 返回一个 Promise 对象,所以 callbacktotal 需要处理 then 函数
  2. 第一个 total 值或者 reduce 初始值需要加工成 Promise 对象 否则会报错
// reduce + async/await
function reduceFun() {
    list.reduce(async (pre, next, index) => {
        return await pre.then(() => setTimeoutFun(next))
    }, Promise.resolve())
}

这里有一个有趣的点——为什么这么写 reduce 生效forEach 不生效

// 这里 forEachFun 依旧是同时执行
function forEachFun() {
    list.forEach(async (num) => {
        await Promise.resolve(num).then(() => setTimeoutFun(num))
    })
}

// 这里 reduceFun 依旧可以等待上一个任务执行完成
function reduceFun() {
    list.reduce(async (pre, next, index) => {
        // return await Promise.resolve().then(() => setTimeoutFun(next)) // 不使用 pre 就同步进行
        return await Promise.resolve(pre).then(() => setTimeoutFun(next)) // 使用 pre 就依次进行
    }, Promise.resolve())
}

从网上找到的观点是:
如果 reduce callbacktotal 参数第一个出现并且参与计算
就可以让异步任务依次进行

听起来很扯,也没有从 MDN 说明和源码 polyfill 上找到合理的解释