async 函数

2019-10-19  本文已影响0人  了凡和纤风

一、含义

ES2017 标准引入了 async 函数,使得异步操作变得更加方便。async 函数时 Generator 函数的语法糖

async 函数对 Generator 函数的改进体现在以下4点:

  1. 内置执行器
    Generator 函数的执行必须靠执行器,所以才有了 co 模块,而 async 函数自带执行器。
  2. 更好的语义
    async 和 await 比起星号 和 yield,语义更加清楚
  3. 更广的适用性
    co 模块约定,yield 命令后面只能是 Thunk 函数或 Promise 对象,而 async 函数的 await 命令后面,可以是 Promise 对象和 原始类型的值(数值、字符串 和 布尔值,但这时等同于同步操作)
  4. 返回的是Promise
    async 函数的返回值是 Promise 对象,这比 Generator 函数的返回值是 Iterator 对象方便了许多。可以用 then 方法指定下一步的操作

二、用法

async 函数返回一个 Promise 对象,可以使用 then 方法添加回调函数。当函数执行的时候,一旦遇到 await 就会先返回等到异步操作完成,再接着执行函数体内后面的语句。

function timeout(ms) {
  return new Promise( resolve => {
    setTimeout(resolve, ms)
  })
}

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

asyncPrint('Hello World', 2000)

async 函数有很多种使用方式

// 函数声明
async function foo() {}

// 函数表达式
const foo = async function() {}

// 对象的方法
let obj = { async foo() {} }
obj.foo().then(...)

// Class 的方法
class Storage {
  constructor() {
    this.cachePromise = caches.open('avatars')
  }

  async getAvatar(name) {
    const cache = await this.cachePromise
    return cache.match(`/avatars/${name}.jpg`)
  }
}

const storage = new Storage()
storage.getAvatar('jake').then(...)

// 箭头函数
const foo = async() => {}

三、语法

async 函数的语法规则总体上来说比较简单,难点是错误处理机制

3.1、返回 Promise 对象

async 函数返回一个 Promise 对象
async 函数内部 return 语句返回的值,会成为 then 方法回调函数的参数

async function f() {
  return 'hello world'
}

f().then(v => console.log(v))
// "hello world"

async 函数内部抛出错误会导致返回的 Promise 对象变为 reject 状态。抛出的错误对象会被 catch 方法回调函数接收到

async function f() {
  throw new Error('出错了')
}


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

3.2、Promise 对象的状态变化

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

3.3、await 命令

正常情况下,await 命令后面是一个 Promise 对象。如果不是,会被转成一共立即 resolve 的 Promise对象

async function f() {
  return await 123
}

f().then(v => console.log(v))
// 123

await 命令后面的 Promise 对象如果变为 reject 状态,则 reject的参数会被 catch 方法的回调函数接收到

async function f() {
  await Promise.reject('出错了')
}

f()
  .then(v => console.log(v))
  .then(e => console.log(e))
// 出错了

只要一个 await 语句后面的 Promise 变为 reject,那么整个 async 函数都会中断执行


有时,我们希望即使前一共异步操作失败,,也不要中断后面的异步操作。这时可以将第一个 await 放在 try...catch 结构里面,这样不管这个异步操作是否成功,第二个 await 都会执行。

async function f() {
  try {
    await Promise.reject('出错了')
  } catch(e) {
    // ...
  }
  return await Promise.resolve('hello world')
}

f()
.then(v => console.log(v))
// hello world

另一种方法是在await 后面的 Promise 对象后添加一个 catch 方法,处理前面可能出现的错误

async function f() {
  await Promise.reject('出错了')
    .catch(e => console.log(e))
  return await Promise.resolve('hello world')
}

f()
.then(v => console.log(v))
// 出错了
// hello world

3.4 错误处理

如果 await 后面的异步操作出错,那么等同于 async 函数返回的 Promise 对象被 reject。

防止出错的方法即使用上面介绍的将其放在 try...catch 代码块之中。如果有多个 await 命令,则可以统一放在 try...catch 结构中。


使用 try...catch 结构,实现多次重复尝试。

const superagent = require('superagent')
const NUM_RETRIES = 3

async function test() {
  let i;
  for (i = 0; i < NUM_RETRIES; ++i) {
    try {
      await superagent.get('http://google.com/this-throws-an-error')
      break;
    } catch(err) {}
  }
  console.log(i) // 3
}
test()

如果 await 操作成功,则会使用 break 语句退出循环;如果失败,则会被 catch 语句捕捉,然后进入下一轮循环

3.5、使用注意点

  1. await 命令后面的 Promise对象的运行结果可能是 rejected,所以最好把 await 命令放在 try...catch 代码块中
async function myFuntion() {
  try {
    await somethingThatReturnsAPromise()
  } catch(err) {
    console.log(err)
  }
}

// 写法二
async function myFunction() {
  await somethingThatReturnsAPromise()
  .catch(err => console.log(err))
}
  1. 多个 await 命令后面的异步操作如果不存在继发关系,最好让他们同时触发
// 写法一
let [foo, bar] = await Promise.all([getFoo(), getBar()])

// 写法二
let fooPromise = getFoo()
let barPromise = getBar()
let foo = await fooPromise
let bar = await barPromise

上面两种写法中,getFoo 和 getBar 都是同时触发,这样就会缩短程序的执行时间。

  1. await 命令只能在 async 函数之中,如果用来普通函数中就会报错

四、async 函数的实现原理

async 函数的实现原理就是将 Generator 函数和 自动执行器 包装在一个函数里

async function fn(args) {
  // ...
}

// 等同于

function fn(args) {
  return spawn(function* () {
    // ...
  })
}

spawn 函数的实现

function spawn(genF) {
  return new Promise(function(resolve, reject) {
    var gen = genF()
    function step(nextF) {
      try {
        var next = nextF()
      } catch(e) {
        return reject(e)
      }
      if (next.done) {
        return resolve(next)
      }
      Promise.resolve(next.value).then(function(v) {
        step(function() {return gen.next(v) })
      }, function(e) {
        step(function() { return gen.throw(e) })
      })
    }
    step(function() { return gen.next(undefined) })
  })
}

五、其他异步处理方法的比较

假定某个 DOM 元素上面,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再继续执行,而返回上一个成功执行的动画的返回值

Promise 的写法

function chainAnimationsPromise(elem, animations) {
  // 变量 ret 同来保存上一个动画的返回值
  var ret = null

  // 新建一个空的 Promise
  var p = Promise.resolve()

  // 使用 then 方法,添加所有动画
  for(var anim of animations) {
    p = p.then(function(val) {
      ret = val
      return anim(elem)
    })
  }
}
  
  // 返回一个不熟了 错误捕捉机制的 Promise
  return p.catch(function(e) {
    /* 忽略错误继续执行 */
  }).then(function() {
    return ret
  })

相较 回调函数的写法大大改进,缺点是本身的语义不强

Generator 函数的写法

function chainAnimationsGenerator(elem, animations) {
  return spawn(function* () {
    var ret = null
    try {
      for(var anim of animations) {
        ret = yield anim(elem)
      }
    } catch(e) {
      /* 忽略错误,继续执行 */
    }
    return ret
  })
}

语义比 Promise 写法更清晰,用户定义的操作全部都出现在 spawn 函数的内部。缺点在于,必须有一个任务运行器自动执行 Generator 函数(spawn),它返回一个Promise 对象,而且必须保证 yield 语句后面的表达式返回一个 Promise

async 函数的写法

async function chainAnimationsAsync(elem, animations) {
  var ret = null
  try {
    for(var anim of animations) {
      ret = await anim(elem)
    }
  } catch(e) {
    /* 忽略*/
  }
  return ret
}

六、实例:按顺序完成异步操作

依次远程读取一组 URL,然后按照读取的顺序输出结果

Promise 的写法如下

function logInOrder(urls) {
  // 远程读取所有 URL
  const textPromise = urls.map(url => {
    return fetch(url).then(response => response.text() )
  })

  // 按次序输出
  textPromise.reduce((chain, textPromise) => {
    return chain.then(() => textPromise)
      .then(text => console.log(text))
  }, Promise.resolve())
}

上面每个 fetch 操作都返回一个 Promise 对象,放入 textPromise 数组。然后,reduce 方法依次处理每个 Promise 对象,并且使用 then 将所有 Promise 对象连起来,因此就可以依次输出结果

async 函数实现

async function logInOrder(urls) {
  for (const url of urls) {
    const response = await fetch(url)
    console.log(await response.text())
  }
}

问题是所有远程操作都是继发,只有前一URL 返回结果后才会去读下一个URL,这样做效率很低,非常浪费时间。

同时发送远程请求

async function logInOrder(urls) {
  // 并发读取远程 URL
  const textPromises = urls.map(async url => {
    const response = await fetch(url)
    return response.text()
  })

  // 按次序输出
  for (const textPromise of textPromises) {
    console.log(await textPromise)
  }
}

上面的代码中,虽然 map 方法的参数是 async 函数,但它是并发执行的,因为只有 async函数内部是继发执行,外部不受影响。后面的 for...of 循环内部使用了 await,因此实现了按顺序输出。

上一篇 下一篇

猜你喜欢

热点阅读