Skip to content

async 函数

ES2017 - async 函数:使异步操作变得更加方便,Generator 函数语法糖

Generator 读取两个文件

js
const fs = require('fs')

const readFile = function (fileName) {
  return new Promise(function (resolve, reject) {
    fs.readFile(fileName, function (error, data) {
      if (error) return reject(error)
      resolve(data)
    })
  })
}

const gen = function* () {
  const f1 = yield readFile('/etc/fstab')
  const f2 = yield readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

改写为 async

写法差异:async 函数将星号(*)替换成 async,将 yield 替换成 await

js
const asyncReadFile = async function () {
  const f1 = await readFile('/etc/fstab')
  const f2 = await readFile('/etc/shells')
  console.log(f1.toString())
  console.log(f2.toString())
}

async 关键字

async 优势

  1. 内置执行器。Generator 函数执行必须靠执行器,而 async 函数自带执行器

  2. 更好的语义。async 和 await,比起星号和 yield,语义更清楚。async 表示异步操作,await 表示等待

  3. 更广的适用性。yield 命令后面只能是 Thunk 函数或 Promise 对象,而 await 命令后面,可以是 Promise 对象和原始类型的值

  4. 返回值是 Promise:async 函数的返回值是 Promise 对象,Generator 返回值为 iterator 对象

async 命令

async 关键字,表明函数内部有异步操作。调用该函数时,会立即返回一个 Promise 对象

js
async function getStockPriceByName(name) {
  const symbol = await getStockSymbol(name)
  const stockPrice = await getStockPrice(symbol)
  return stockPrice
}

getStockPriceByName('goog').then(function (result) {
  console.log(result)
})

async 函数返回的是 Promise 对象,所以可以作为 await 参数

js
async function timeout(ms) {
  await new Promise((resolve) => {
    setTimeout(resolve, ms)
  })
}

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

asyncPrint('hello world', 50)

async 函数有多种使用形式

js
// 函数声明
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 函数内部抛出错误,会导致返回的 Promise 对象变为 reject 状态,错误会被 catch 方法回调接收

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

f().then(
  (v) => console.log('resolve', v),
  (e) => console.log('reject', e)
)

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

js
async function getTitle(url) {
  let response = await fetch(url)
  let html = await response.text()
  return html.match(/<title>([\s\S]+)<\/title>/i)[1]
}
getTitle('https://tc39.github.io/ecma262/').then(console.log)
// "ECMAScript 2017 Language Specification"

getTitle 中三个操作全部完成后,才执行 then

await 命令

await 命令后面是一个 Promise 对象,返回该对象的结果。如果不是 Promise 对象,就直接返回对应的值

js
async function f() {
  // 等同于
  // return 123;
  return await 123
}

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

await 命令后面是一个 thenable 对象(即定义了 then 方法的对象)

js
class Sleep {
  constructor(timeout) {
    this.timeout = timeout
  }
  then(resolve, reject) {
    const startTime = Date.now()
    setTimeout(() => resolve(Date.now() - startTime), this.timeout)
  }
}

;(async () => {
  const sleepTime = await new Sleep(1000)
  console.log(sleepTime)
})()
// 1000

例子:实现 sleep

js
function sleep(interval) {
  return new Promise((resolve) => {
    setTimeout(resolve, interval)
  })
}

async function start() {
  await sleep(1000)
}
start()

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

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

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

注意,抛出的错误会改变 async 函数 返回的 Promise 状态,所以不需要 return 也会被外部捕获。

await 后面 Promise 变为 reject 状态,会中断整个 async 函数

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

前一个异步错误,不中断后面的解决方案

  1. 通过 try...catch 捕获错误
js
async function f() {
  try {
    await Promise.reject('出错了')
  } catch (e) {}
  return await Promise.resolve('hello world')
}
  1. Promise 后面 添加 catch 方法
js
async function f() {
  await Promise.reject('出错了').catch((e) => console.log(e))
  return await Promise.resolve('hello world')
}

await 后面的异步操作抛出错误,等同于 async 返回 Promise 被 reject

js
async function f() {
  await new Promise(function (resolve, reject) {
    throw new Error('出错了')
  })
}

f()
  .then((v) => console.log(v))
  .catch((e) => console.log(e))

防止出错的方法,也是将其放在 try...catch 代码块之中

应用:通过 try...catch 重复请求

js
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()

注意点

  1. await 后面 Promise 会抛出异常,所以 await 通常用 try...catch 包裹

  2. 多个 await 异步操作

js
function asyncFunc(v, t) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      resolve(v)
    }, t * 1000)
  })
}

const list = [
  ['first', 3],
  ['second', 2],
  ['three', 1]
]

并发执行

js
// 1.Promise.all
async function runAsync1_1() {
  const promiseQueue = list.map(([x, y]) => asyncFunc(x, y))
  const res = await Promise.all(promiseQueue)
  console.log('res: ', res)
}
// runAsync1_1()

// 2.手动
async function runAsync1_2() {
  const p1 = asyncFunc(list[0][0], list[0][1])
  const p2 = asyncFunc(list[1][0], list[1][1])
  const p3 = asyncFunc(list[2][0], list[2][1])
  const result1 = await p1
  const result2 = await p2
  const result3 = await p3
  console.log('res :', [result1, result2, result3])
}
// runAsync1_2()

继发执行

js
// 1. reduce
async function runAsync2_1() {
  const res = []
  await list.reduce(async (pre, cur) => {
    // 保证执行顺序
    await pre
    const result = await asyncFunc(cur[0], cur[1])
    res.push(result)
  }, undefined)
  console.log('res', res)
}
// runAsync2_1()

// 2. for...of
async function runAsync2_2() {
  const res = []
  for (const [x, y] of list) {
    const result = await asyncFunc(x, y)
    res.push(result)
  }
  console.log('res', res)
}
runAsync2_2()
  1. await 命令只能用在 async 函数之中,如果用在普通函数,就会报错

  2. async 函数可以保留运行堆栈。

js
const a = () => {
  b().then(() => c())
}

上述代码,b 运行时, a 已经结束,b 所在上下文栈已经消失

js
const a = async () => {
  await b()
  c()
}

async 会通过 await 暂停执行,所以上下文还在

async 实现原理

将 Generator 函数和自动执行器,包装在一个函数里

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

// 等同于

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

spawn 函数就是自动执行器

js
// spawn 函数
function spawn(gen) {
  const it = gen()

  const next = (data) => {
    const result = it.next(data)

    if (result.done) return result.value

    result.value.then((res) => {
      next(res)
    })
  }
  next()
}
js
function mockFn() {
  return spawn(function* () {
    const res1 = yield asyncFunc('fn1', 1000)
    const res2 = yield asyncFunc('fn2', 2000)
    const res3 = yield asyncFunc('fn3', 3000)
    // ...
    console.log(res1)
    console.log(res2)
    console.log(res3)
  })
}

spawn 函数 promise 版本

  1. 返回值 promise
  2. yield 后面可以是 Promise 或原始值
js
function spawnPromise(gen) {
  return new Promise((resolve, reject) => {
    const it = gen()
    const next = (func) => {
      let result
      try {
        result = func()
      } catch (error) {
        reject(error)
      }
      if (result.done) {
        resolve(result.value)
      }
      // 保证原始值也有then方法
      Promise.resolve(result.value)
        .then((res) => {
          next(() => it.next(res))
        })
        .catch((err) => {
          next(() => it.thrown(err))
        })
    }
    next(() => it.next(undefined))
  })
}

异步处理方法比较

场景:某个 DOM 元素,部署了一系列的动画,前一个动画结束,才能开始后一个。如果当中有一个动画出错,就不再往下执行,返回上一个成功执行的动画的返回值。

Promise 实现

优点:相比回调更清除

缺点:Promise API 偏多,操作语义不容易看出

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

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

  // 使用then方法,添加所有动画
  for (let anim of animations) {
    p = p.then(function (val) {
      ret = val
      return anim(elem)
    })
  }

  // 返回一个部署了错误捕捉机制的Promise
  return p
    .catch(function (e) {
      /* 忽略错误,继续执行 */
    })
    .then(function () {
      return ret
    })
}

Generator 函数的写法

优点:语义更清楚

缺点:需要执行器,yield 后面必须为 Promise

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

async 函数

优点:语义清除,无需执行器

js
async function chainAnimationsAsync(elem, animations) {
  let ret = null
  try {
    for (let anim of animations) {
      ret = await anim(elem)
    }
  } catch (e) {
    /* 忽略错误,继续执行 */
  }
  return ret
}

实战: 顺序执行异步操作

一组异步操作,需要按照顺序完成

Promise 写法:先将 Promise 存入数组,然后遍历数组

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

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

async 函数实现:通过 await 保证执行。相较 Promise 写法,需要前面请求完成,才能执行下一个,效率更低。

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

优化:并发请求

js
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)
  }
}

顶层 await

早期的语法,await 命令只能出现在 async 函数内部,否则都会报错

ES2022:允许在模块的顶层独立使用 await 命令

js
// awaiting.js
let output
async function main() {
  const dynamic = await import(someMission)
  const data = await fetch(url)
  output = someProcess(dynamic.default, data)
}
main()
export { output }
js
// usage.js
import { output } from './awaiting.js'

function outputPlusValue(value) {
  return output + value
}

console.log(outputPlusValue(100))
setTimeout(() => console.log(outputPlusValue(100)), 1000)

问题:执行结果,完全取决于执行的时间。如果 awaiting.js 里面的异步操作没执行完,加载进来的 output 的值就是 undefined

解决:原始模块输出 Promise,通过 Promise 状态判断异步操作是否完成

js
// awaiting.js
let output
export default (async function main() {
  const dynamic = await import(someMission)
  const data = await fetch(url)
  output = someProcess(dynamic.default, data)
})()
export { output }
js
// usage.js
import promise, { output } from './awaiting.js'

function outputPlusValue(value) {
  return output + value
}

promise.then(() => {
  console.log(outputPlusValue(100))
  setTimeout(() => console.log(outputPlusValue(100)), 1000)
})

这个方案需要使用者遵守一个额外的使用协议,按照特殊的方法使用这个模块。新模块有对外输出,又要按照同样的规则

顶层的 await 命令就是为了解决这个问题,保证只有异步操作完成,模块才会输出值。

js
// awaiting.js
const dynamic = import(someMission)
const data = fetch(url)
export const output = someProcess((await dynamic).default, await data)

注意:顶层 await 只能用在 ES6 模块,不能用在 CommonJS 模块。因为 CommonJS 模块的 require()是同步加载,如果有顶层 await,就没法处理加载

顶层 await 的一些使用场景

js
// import() 方法加载
const strings = await import(`/i18n/${navigator.language}`)

// 数据库操作
const connection = await dbConnector()

// 依赖回滚
let jQuery
try {
  jQuery = await import('https://cdn-a.com/jQuery')
} catch {
  jQuery = await import('https://cdn-b.com/jQuery')
}

多个包含顶层 await 命令的模块,加载命令是同步执行

js
// x.js
console.log('X1')
await new Promise((r) => setTimeout(r, 1000))
console.log('X2')

// y.js
console.log('Y')

// z.js
import './x.js'
import './y.js'
console.log('Z')

// X1、Y、X2、Z

相关链接

[-] async 函数