ES6 async 函数详解

发布于 2024-12-05 12:50:57 字数 8260 浏览 12 评论 0

async 函数是基于 Generator 函数实现的,也就是说是 Generator 函数的语法糖。在之前的文章有介绍过 Generator 函数语法和异步应用,如果对其不了解的,可以先看看关于 Generator 函数的文章,这样学习 async 函数的难度就不会太大。

接下来会用一些篇幅说明一下 async 函数,文末会给出 async 函数的参考学习文章。


含义

我们知道,调用 Generator 函数不会立即执行,而是返回遍历器对象。疲于手动执行遍历器对象,因此就有了 thunk(thunkify) 函数结合 run 函数来实现自动流程管理。或者,使用 co 模块来实现自动流程管理,使 Generator 函数的使用更加方便。

而 async 函数 ES2017 标准引入的语法,是 Generator 函数的语法糖,因此其相对于 Generator 函数,具有以下基本特点。

  • 内置执行器: 使用 async 函数可以像使用普通函数一样,直接调用即可执行。不用像 Generator 函数一样使用 co 模块来实现流程控制。
  • 语义化更强: async 关键字表示是一个异步的函数,await 表示需要等待执行。相对于 yield 表达式,语义化更强。
  • 返回值是 Promise: async 函数返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便多了,可以使用 then 方法来指定下一步的操作。

基本语法

使用 async 关键字表明函数是一个 async 函数,内部使用 await 关键字表明需要等待异步任务结束后才继续往下执行。

async functionas () {
  return 123
}
as().then(data => {
  console.log(data)
})

从上面代码可以看出,调用 async 函数会返回 Promise 对象,返回值可以作为 then 方法成功处理函数的参数值。

如果在 async 内部如果抛出错误或者出现异常,会被 then 方法的错误处理函数捕获或者 catch 方法捕获。

async functionas () {
  throw new Error('出错拉!')
}
as().then(data => {
  console.log(data)
}).catch(err => {
  console.log(err)
})  // Error: xixi, catch 方法捕获到错误

另外,async 函数内部可以使用 await 关键字,表示后面的表达式是异步任务。await 关键字后边的表达式可以是一个 Promise 对象,或者简单(复杂) 数据类型(Number, String, RegExp, Boolean, Array, Objext)。如果是简单(复杂) 数据类型,async 函数会隐式调用 Promise.resolve 方法将其转换为 Pormise 对象。

functionfoo () {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      resolve('async')
    }, 1000)
  })
}
async functionas () {
  const data = await foo()  //foo 函数使用 setTimeout 来模拟异步。
  console.log(data)
}
as()  // async

async functionas () {
  return await 123   //如果是其他数据类型,也是如此。
}
as().then(data => {
  console.log(data)
})  // 123

如果 await 关键字后面的表达式是非 Promise、非 thenable 的普通的值,则会隐式调用 Promise.resolve 方法将其转换为 Promise 对象,await 关键字会在内部调用 then 方法将 resolve 的值返回。

await 内部实现大致如下
function await (data) {
  return new Promise((resolve, reject) => {
    resolve(data)
  }).then(data => {
    return data
  })
}

总之,await 关键字是 then 方法的语法糖,会将 resolve 的值传递出来。

另外,如果在 await 关键字后的表达式抛出了错误,会使 async 函数返回的 Promise 对象从 pending 状态转变为 reject 状态,进而被 catch 方法捕获到错误。

functionfoo () {
  throw new Error('err')
}
async functionas () {
  await foo()
}
as().then(data => {})
    .catch(err => {
      console.log(err);
    })  // as 函数返回的 Promise 对象从 pending 状态变为 reject 状态。

如果某个 await 关键字后面的表达式抛出错误,async 函数的状态就会变为 reject,那么函数就会暂停执行,后面的表达式就不会在继续执行。因为 Promise 函数有一个特点是, 一旦状态改变,就不会再变,之后在调用也是保持同一个状态。

functionfoo () {
  throw new Error('err')
}
async functionas () {
  await foo()
  return Promise.resolve('succ') // 不会执行到这里,因为 Promise 对象的状态一旦改变就不会在变了,因此不执行。
}
as().then(data => {})
    .catch(err => {
      console.log(err);
    })

因为 async 函数默认情况下返回的是 Promise 对象,因此可以将 async 函数作为 await 关键字后面的表达式。async 函数调用另一个 async 函数会更加方便,不会像 Generator 函数需要使用 yield* 表达式来调用。

async functionfoo () {
  return Promise.resolve('async')
}
async functionas () {
  return await foo()   // 调用 foo 函数会返回 Promise 对象
}
as().then(data => {
  console.log(data)
})

另外,如果 async 函数内部没有抛出错误,函数正常执行。那么每一个 await 关键字后面的异步任务会继发执行。也就是说,一个异步任务结束之后才会执行另外一个异步任务,而不是并发执行。

async functionfoo () {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      resolve(10)
    }, 1000)
  })
}
async functionbar () {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      resolve(20)
    }, 2000)
  })
}
async functionas () {
  let t1 = Date.now()
  const a = await foo()
  const b = await bar()
  let t2 = Date.now()
  console.log(t2 - t1)  // 有误差,大概 3004ms
  return a + b
}
as().then(data => {
  console.log(data)   // 大概 3s 后输入 30
})

如果两个异步任务互不依赖,如果按照上面的代码,两个异步任务继发执行,这样做的缺点是时间浪费了。本来 200ms 可以完成的两个异步任务,却用了 400ms。因此可以让两个互不依赖的异步任务同时触发。有两种方法:

// 方法一:
async functionas () {
  const t1 = Date.now()
  const [fo, ba] = [foo(), bar()]
  // 以上两个函数同时执行,并将结果作为 await 关键字的表达式
  const a = await fo
  const b = await ba
  const t2 = Date.now()
  console.log(t2 - t1)
  return a + b
}

// 写法二:结合使用 Promise.all 等待所有异步任务完成后才会返回
async functionas () {
  const t1 = Date.now()
  const arr = await Promise.all([foo(), bar()])
  const t2 = Date.now()
  console.log(t2 - t1)
  return arr[0] + arr[1]
}
as().then(data => {
  console.log(data)  // 30
})

错误处理

由于 async 函数内部的异步任务一旦出现错误,那么就等同于 async 函数返回的 Promise 对象被 reject。因此,为了防止异步任务出现错误,可以使用 try...catch 来捕获错误,使 async 函数内部可以正常执行。

async functionas () {
  let a = 0
  let b = 0
  try {
    a = await foo()
    b = await bar()
  } catch (e) {}
  return a + b
}
as().then(data => {
  console.log(data) // 30
})

我们知道, try...catch 只能用于处理同步的操作,对于异步任务无法捕获到错误。而 await 关键字能够暂停函数处理,等待异步任务结束之后返回。因此在 async 函数中使用 try...catch 结合 await 关键字捕获异步错误是一个不错的方法。

异步应用

我们来看看使用 Promise、Generator、async 来实现异步应用的差别。接下来会使用 setTimeout 来模拟异步。

先来看两个基础函数

function foo (obj) {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      let data = {
        height: 180
      }
      data = Object.assign({}, obj, data)
      resolve(data)
    }, 1000)
  })
}
function bar (obj) {
  return new Promise((resolve, reject) => {
    window.setTimeout(() => {
      let data = {
        talk () {
          console.log(this.name, this.height);
        }
      }
      data = Object.assign({}, obj, data)
      resolve(data)
    }, 1500)
  })
}

两个函数内部都返回了 Promise 实例对象,通过 Object.assign 来合并传递过来的参数。

首先看看纯 Promise 对象的实现。

functionmain () {
  return new Promise((resolve, reject) => {
    const data = {
      name: 'keith'
    }
    resolve(data)
  })
}
main().then(data => {
  foo(data).then(res => {
    bar(res).then(data => {
      return data.talk()   // keith 180
    })
  })
})

调用过程中就是在不断使用 then 方法,不够直观,操作本身的语义不太容易看出来。而且有可能出现回调地狱的风险。

接下来看看 Generator 函数的实现。由于 Generator 函数的调用需要手动执行,因此写了 run 函数来实现流程自动控制。

function *gen () {
  const data = {
    name: 'keith'
  }
  const fooData = yield foo(data)
  const barData = yield bar(fooData)
  return barData.talk()
}
function run (gen) {
  const g = gen()
  const next = data => {
    let result = g.next(data)
    if (result.done) return result.value
    result.value.then(data => {
      next(data)
    })
  }
  next()
}
run(gen)

使用 run 函数来实现自动流程控制,Generator 函数的好处相对于 Promise 对象来说,使得异步的过程同步化,同时少了回调地狱的风险。但是缺点是需要使用像 run 函数或者 co 模块来实现流程控制。

接下来使用 async 函数来实现看看。

async functionmain () {
  const data = {
    name: 'keith'
  }
  const fooData = await foo(data)
  const barData = await bar(fooData)
  return barData
}
main().then(data => {
  data.talk()
})

从上面代码中,可以看出,使用 async 函数的代码量最少,而且使得异步过程同步化,更进一步,async 函数内置执行器。调用的方法更加简洁。


ok,关于 async 函数的相关差不多就这样了,稍微总结一下。

  1. async 函数是基于 Generator 函数实现的,是 Generator 函数的语法糖。其内置执行器,调用后返回 Promise 对象,因此可以像普通韩式一样使用。
  2. async 函数内部抛出错误或者 await 关键字后面的表达式抛出错误,会使 async 函数返回的 Promise 对象从 pending 状态变为 reject 状态,从而可以被 catch 方法捕获错误。而且,Promise 对象的状态一旦改变就不会再变,之后的异步任务就不会执行了。
  3. await 关键字后面的表达式可以是 Promise 对象,也可以是其他数据类型。如果是其他数据类型,则会通过 Promise.resolve 将其转换为 Promise 对象
  4. async 函数内部如果有多个 await 关键字,其后的异步任务会继发执行。如果每一个异步任务不相互依赖,则可以使用 Promise.all 让其并发执行,这样可以在同样的时间里完成多个异步任务,提高函数执行效率。
  5. 对于 async 内部抛出的错误,可以使用 try...catch 来捕获异常。虽然 try...catch 只能用于捕获同步任务,但是 await 关键字可以使得异步任务同步化,因此可以结合 try...catch 和 await 关键字捕获异步任务。

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

耶耶耶

暂无简介

0 文章
0 评论
25 人气
更多

推荐作者

已经忘了多久

文章 0 评论 0

15867725375

文章 0 评论 0

LonelySnow

文章 0 评论 0

走过海棠暮

文章 0 评论 0

轻许诺言

文章 0 评论 0

信馬由缰

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文