logo

JavaScript Promise - 异步函数 (4/6)

JavaScript 设计 promise 的目的是将其用作高级语言特性的底层工具。异步函数 (async) 就是这样一个高级语言特性,它抽象了 promise,使我们可以用编写常规代码的自上而下的方式来编写 promise 代码,而不必担心如何追踪管理 promise 及其 handler。

如何定义异步函数

异步函数可以用于任何同步函数能用的地方。大多数情况下,在函数或方法定义前添加一个 async 关键字就可以使其变为异步。下面是定义异步函数/方法的几种常见例子:

// 异步函数
async function doSomething() {}

// 异步箭头函数
const doSomething = async () => {}

// 对象中的异步方法
const object = {
    async doSomething() {}
}

// 类中的异步方法
class MyClass {
    async doSomething() {}
}

async 关键字表示这个函数/方法是异步的。对于 JavaScript 引擎来说,提前知道一个函数是否是异步函数是很重要的,因为它的行为与同步函数不同。

异步函数的独特之处

异步函数有四个主要的特点:

总是返回 promise

这和前面讲过的 promise 的 then()finally() 等方法是一样的:返回值不是 promise 的也会被 Promise.resolve() 方法包装成 promise。因为 async 关键字是在 promise 上做了一层抽象,但不改变 promise 本身。

async function doSomething() { return 2 }
const p1 = doSomething()

console.log(p1 instanceof Promise) // true
console.log(typeof p1 === 'number') // false
p1.then(v => console.log(v)) // 2

如果把一个外部的 promise 传递给 return,那么这个 promise 不会被直接返回,只是返回其拷贝:

const p = Promise.resolve(2)
async function doSomething() { return p }

const p1 = doSomething()
console.log(p === p1) // false

如果不指定返回值,那么返回值默认是一个 promise,求值这个 promise 会得到 undefined

async function doSomething() {}

const p = doSomething()
console.log(p instanceof Promise) // true
p.then(v => console.log(v)) // undefined

这里的重点是:无论你在异步函数中做了什么乱七八糟的操作,它都会返回一个 promise

抛出的错误

上面说了,异步函数无论如何都会返回 promise。抛出的错误也不例外,只不过返回的是一个失败的 promise。这里的重点是:我们不能用 try-catch 来捕获异步函数抛出的错误

async function throwError() { throw new Error("Error") }

try {
    throwError()
    console.log("Didn't catch error")
} catch (e) {
    // 这里永远不会执行
    console.log("Never caught error")
}

因为异步函数返回的是 promise,我们只能用 promise 的方式来处理错误:

throwError().catch(reason => console.error(reason.message)) // Error

为了确保异步函数总是返回 promise,让我们有一个一致的方式来处理返回值,JavaScript 引擎也挺不容易的。

await 表达式

await 表达式是为了让使用 promise 更简单。使用 promise 时如果我们想要获得最终值,那么需要在 then() 等方法中调用 resolve 或 reject handler,await 的工作即隐式帮我们调用 handler。所以通过 await 我们直接就可以获得最终值,这使我们可以写出类似同步函数自上而下的代码,也使我们可以使用 try-catch 捕获错误。

// 不使用 await
function getJSONData(url) {
  return fetch(url)
    .then((response) => {
      if (response.ok) {
        return response.json()
      } else {
        throw new Error(`${response.statusText}`)
      }
    })
    .catch((reason) => console.error(reason.message))
}

// 使用 await
async function getJSONData(url) {
  try {
    const response = await fetch(url)
    if (response.ok) {
      return await response.json()
    } else {
      throw new Error(`${response.statusText}`)
    }
  } catch (e) {
    console.error(e.message)
  }
}

await 还可以和非 promise 的值一起使用,同样,这些值通过 Promise.resolve() 被隐式包装为 promise。

async function doSomething() {
    return await 2
}

doSomething().then(v => console.log(v)) // 2

await 表达式作用在单个 promise 上,然而上一篇讲述的 Promise.all() 等四个方法作用在多个 promise 上,且返回单个 promise,这意味着 await 和这些方法结合使得 await 可以作用在多个 promise 上。

async function doSomething() {
    try {
        return await Promise.all([p1, p2, p3])
    } catch(e) {
        console.error(e.message)
    }
}

这里 Promise.all() 方法先执行完毕,然后 await 作用在其结果上。

for-await-of 循环

可迭代对象 (iterable) 是实现了 Symbol.iterator 方法的对象,异步可迭代对象 (async iterable) 是实现了 Symbol.asyncIterator 方法的对象。for-await-of 循环作用在可迭代对象或异步可迭代对象上,如果是异步对象,则等待 promise 成功然后获取其值,再继续下一轮循环。

const p1 = Promise.resolve(2)
const p2 = Promise.resolve(3)
const p3 = Promise.resolve(4)

for await (const v of [p1, p2, p3]) {
  console.log(v)
}

// 也可以作用在元素为非 promise 的可迭代对象上,众所周知,这些元素会被隐式转为 promise
for await (const v of [1, 2, 3]) {
  console.log(v)
}

Node.js 中最常用的异步可迭代对象是 ReadStreamReadStream 对象用于从可能不可用的源中定期读取数据。如网络请求,读取大文件或事件流。

import fs from "node:fs"

async function readCompleteTextStream(readable) {
  readable.setEncoding("utf8")

  try {
    let data = ""
    // 如果可迭代对象的 promise 失败,则 for-await-of 抛出错误
    for await (const chunk of readable) {
      data += chunk
    }
    return data
  } catch (e) {
    console.error(e.message)
  }
}

const stream = fs.createReadStream("data.txt")
readCompleteTextStream(stream).then((text) => console.log(text))

顶层 await 表达式

await 表达式还可以用在异步函数之外、 JavaScript 模块内的顶层。从本质上讲,JavaScript 模块在默认情况下充当包裹整个模块的异步函数。这允许你直接调用基于 promise 的函数,比如使用 import() 函数:

// 静态 import
import something from "./file.js"

// 动态 import
const filename = "./another-file.js"
const somethingElse = await import(filename)

使用顶层 await,可以在静态加载模块的同时动态加载模块。(动态加载的模块也允许我们动态地构造模块说明符,这在静态导入中是不可能的。)

当 JavaScript 引擎遇到顶层 await 时,JavaScript 模块的执行将暂停,直到 promise 执行完毕。如果被暂停模块的父模块有静态导入需要处理,那么那些导入将继续。但在这种情况下,不能保证兄弟模块的加载顺序,不过大多数情况下,该顺序应该无关紧要。

顶层 await 不能用在 JavaScript 脚本中,必须使用 import 加载代码或者使用 <script type="module">

总结

简而言之: