JavaScript的async/await

ES6入门教程-阮一峰:async 函数

1. async 函数介绍

ES2017 新规中引入了 async 函数,async 函数其实就是 Generator 函数的语法糖,只是省去了 next 方法递归的过程。
async 函数其实只是对 Generator 函数的改进而已。

  1. 内置执行器
    a. Generator 函数的执行必须依靠执行器,而 async 自带了执行器,所以 async 函数的执行和普通函数一样,直接调用即可。

  2. 更好的语义:
    a. async 表示函数里有异步操作
    b. await 翻译过来就是等待的意思,需要等待后面的 Promise 执行结束才会执行下一步

  3. 更广的实用性
    a. co 模块规定,yield 后面只能是 Thunk 函数或 Promise 对象,而 async 函数的await 命令后面,可以是 Promise 对象和原始类型的值 ( 数值、字符串、布尔值,但会自动转换 Promise.resolved() )

  4. 返回的是 Promise 对象
    a. async 函数的返回值是 Promise 对象,同样可以使用 then 方法来指定下一步操作。这比 Generator 函数返回的 Iterator ( 迭代器 ) 对象方便多了。

// async 的返回值是 Promise,所以同样可以使用 then() 方法来指定回调函数
async function p1() {
  // 返回的是字符串,会自动转换 ( Promise.resolve() )
  return '成功' 
}

console.log(p1());
// Promise {<fulfilled>: '成功'}

// 同样可以调用 then 方法
p1().then(value => {
  console.log(value);
})
// 举个栗子:多少毫秒后输出一个值
function timeout(ms) {
  // 返回一个 Promise
  return new Promise((resolve) => {
    setTimeout(() => {
      resolve()
    }, ms)
  })
}

async function asyncPrint(value, ms) {
  await timeout(ms)
  console.log(value)
}

asyncPrint('hello world!', 2000)

async 函数返回的 Promise 对象,必须等到内部所有的 await 命令后面的 Promise 对象执行完毕,才会发生状态改变,除非遇到 return 语句或者抛出错误。

async function fn() {
  await new Promise((resolve) => {
    setTimeout(() => {
      console.log(1)
      resolve()
    }, 2000)
  })

  // 抛出错误
  // throw new Error('Error')

  // return 语句
  // return 'return 语句'

  await new Promise((resolve) => {
    setTimeout(() => {
      console.log(2)
      resolve()
    }, 2000)
  })
}

fn().then((value) => {
  console.log(value)
})

2. await 命令

await 翻译过来就是等待的意思,需要等待命令后面的 Promise 执行结束才会执行下一步。

正常情况下,await 命令后面是一个 Promise 对象,返回该对象的结果。如果不算 Promise 对象,直接返回对应的值。

async function fn() {
  let a = await 'a'
  console.log(a)

  let b = await 1
  console.log(b)

  let c = await setTimeout(()=>{}, 0)
  console.log(c);

  let d = await { a: 1 }
  console.log(d)

  let e = await function fun() {}
  console.log(e)
}


fn()
// a
// 1
// Timeout 对象
// { a:1 }
// [Function fun]

如果 await 命令后面的 Promise 对象变为 rejected 状态,则 reject 的参数会被 catch 方法的回调函数接收。并且后续的代码都将不会再执行,async 函数也会中断执行。

async function fn() {
  await Promise.reject('出错了')
  await Promise.resolve('hello world')  // 不会执行
}

fn().then((v) => {
  console.log(v)
}).catch((e) => {
  console.log(e)
})
// 出错了

如果我们希望前面的异步操作失败,依然不影响后面的异步操作时。

  1. 第一种解决方法:可以将 await 放在 try...catch 结构中,这样不管这个异步是否成功,后续的 await 都会接着执行。
  2. 第二种解决方法:直接在 Promise 对象上跟随 catch 方法,处理自身可能出现的错误。
// 第一种解决方法
async function fn() {
  try {
    await Promise.reject('出错了')
  } catch (e) {/* 错误在这里进行处理 */}

  return await Promise.resolve('hello world')
}

fn()
  .then((v) => {
    console.log(v)
  })
// hello world

/****************************/

// 第二种解决方法
async function fn() {
  await Promise.reject('出错了').catch((e) => {/* 错误在这里进行处理 */})
  
  return await Promise.resolve('hello world')
}

fn()
  .then((v) => {
    console.log(v)
  })
// hello world

3.错误处理

如果 await 后面的异步操作出错,就相当于 async 函数返回的 Promise 对象被 reject。

async function fn() {
  await new Promise((resolve) => {
    throw new Error('出错了')
  })
}

fn()
  .then((v) => console.log(v))
  .catch((e) => console.log(e))
  // Error: 出错了

上文提到,可以将 await 放在 try...catch 语句中,解决问题。多个 await 可以统一放在 try...catch 代码块中。

async function fn() {
  try {
    await fn1()
    await fn2()
    await fn3()
  } catch (e) {
    console.log(e)
  }
}

3.1 多次重复尝试案例

function ajax(url) {
  return new Promise((resolve, reject) => {
    let xhr = new XMLHttpRequest()
    xhr.open('GET', url)
    xhr.send()

    xhr.onload = function () {
      if (this.status >= 200 || this.status < 300) {
        resolve(JSON.parse(this.response))
      } else {
        reject('加载失败')
      }
    }
  })
}
const NUM_RETRIES = 3

async function test() {
  let i = 0
  for (; i < NUM_RETRIES; ++i) {
    try {
      let aa = await ajax('https://api.apiopen.top/api/sentences')
      console.log(aa)
      // 如果 await 操作成功,就会使用 break 语句跳出循环,
      // 如果失败,错误被 catch 语句捕获,然后进入下一轮循环
      break
    } catch (err) {}
  }
  console.log(i)  // 0
}

test()

4. await 和并行

在下面的代码中, 三个await 命令是互不关联的, 可却只能等上一个 await 命令结束才能执行下一个, 比较耗时。( 全部执行完, 需要 3s )

function timeout(text, ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(text);
      resolve()
    }, ms)
  })
}

async function fn() {
  await timeout(1, 1000)
  await timeout(2, 1000)
  await timeout(3, 1000)
}


fn()
// 1
// 2
// 3

// 每个 await 命令都需要等上一个 await 命令结束才能开始

所以我们完全可以让他们并行执行, 只需要在 Promise.all() 方法前加上 await 就可以完成。( 全部执行完只需要 1s )

// 方法一:Promise.all() 方法
function timeout(text, ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(text);
      resolve()
    }, ms)
  })
}

async function fn() {
  await Promise.all([timeout(1, 1000), timeout(2, 1000), timeout(3, 1000)])
}


fn()
// 1
// 2
// 3
// 1 2 3 是一起输出的, 总耗时时间为 1000ms

/*******************************************************/

// 方法二
function timeout(text, ms) {
  return new Promise((resolve) => {
    setTimeout(() => {
      console.log(text)
      resolve()
    }, ms)
  })
}

async function fn() {
  let a = timeout(1, 1000)
  let b = timeout(2, 1000)
  let c = timeout(3, 1000)

  await a
  await b
  await c
}

fn()
// 1
// 2
// 3
// 1 2 3 是一起输出的, 总耗时时间为 1000ms
posted @ 2022-10-24 22:56  如是。  阅读(152)  评论(0编辑  收藏  举报